Skip to content

agent: @U0AJM7X8FBR implement the wildest craziest most WOW demoable feature. M#328

Open
recoup-coding-agent wants to merge 6 commits intotestfrom
agent/-u0ajm7x8fbr-implement-the-wil-1774118338794
Open

agent: @U0AJM7X8FBR implement the wildest craziest most WOW demoable feature. M#328
recoup-coding-agent wants to merge 6 commits intotestfrom
agent/-u0ajm7x8fbr-implement-the-wil-1774118338794

Conversation

@recoup-coding-agent
Copy link
Copy Markdown
Collaborator

@recoup-coding-agent recoup-coding-agent commented Mar 21, 2026

Automated PR from coding agent.

Summary by CodeRabbit

  • New Features

    • Added an Artist Intelligence Pack: AI-generated artist reports, marketing copy, social captions, playlist/email pitches, press opener, top-track summary, and a formatted markdown report — available via a new API endpoint and integrated tool.
  • Documentation

    • Expanded in-code JSDoc parameter annotations across many modules for clearer developer-facing docs.

Recoup Agent and others added 2 commits March 21, 2026 18:37
…plexity → marketing copy

New domain: lib/artistIntel/
- generateArtistIntelPack: orchestrates 3 parallel data sources into a complete pack
- getArtistSpotifyData: fetches artist profile + top tracks with 30-sec preview URLs
- getArtistMusicAnalysis: runs 4 MusicFlamingo NVIDIA presets (catalog_metadata,
  audience_profile, playlist_pitch, mood_tags) in parallel on a Spotify preview URL
- getArtistWebContext: Perplexity web research for recent press and streaming news
- buildArtistMarketingCopy: AI synthesis → playlist pitch email, social captions,
  press release opener, key talking points
- validateArtistIntelBody: Zod input validation
- generateArtistIntelPackHandler: route handler with validateAuthContext

New API endpoint: POST /api/artists/intel (artist_name → complete intelligence pack)

New MCP tool: generate_artist_intel_pack — available in the AI agent chat UI

10 unit tests, all green. No lint errors on new files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…feature. M

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
recoup-api Ready Ready Preview Mar 21, 2026 10:47pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 21, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds JSDoc across many modules and introduces an Artist Intelligence Pack feature: new artist-intel library modules (Spotify fetch, MusicFlamingo analysis, web context, marketing-copy generation, formatting), a POST API route with CORS/OPTIONS, request validation and handler plumbing, and MCP tool registration to invoke pack generation.

Changes

Cohort / File(s) Summary
Artist Intelligence API & Route
app/api/artists/intel/route.ts
New Next.js route exposing OPTIONS (CORS) and POST delegating to generateArtistIntelPackHandler.
Artist Intelligence Core
lib/artistIntel/getArtistSpotifyData.ts, lib/artistIntel/getArtistMusicAnalysis.ts, lib/artistIntel/getArtistWebContext.ts, lib/artistIntel/buildArtistMarketingCopy.ts, lib/artistIntel/generateArtistIntelPack.ts, lib/artistIntel/formatArtistIntelPackAsMarkdown.ts, lib/artistIntel/analyzeCatalogDepth.ts, lib/artistIntel/computeArtistOpportunityScores.ts, lib/artistIntel/getRelatedArtistsData.ts
New modules: Spotify fetches, MusicFlamingo analysis, Perplexity web context, marketing-copy generation, orchestration, catalog analysis, opportunity scoring, peer benchmarking, and markdown formatter.
Handler & Validation
lib/artistIntel/generateArtistIntelPackHandler.ts, lib/artistIntel/validateArtistIntelBody.ts
Added POST handler that parses JSON, enforces auth, validates body (Zod), calls generator, and returns JSON responses with CORS (400/404/200 flows).
MCP Tooling
lib/mcp/tools/artistIntel/index.ts, lib/mcp/tools/artistIntel/registerGenerateArtistIntelPackTool.ts, lib/mcp/tools/index.ts
New MCP tool registration generate_artist_intel_pack with Zod input schema, account resolution, and wiring into global tool registration.
Markdown Formatting
lib/artistIntel/formatArtistIntelPackAsMarkdown.ts
New formatter that renders an ArtistIntelPack into a single markdown document with conditional sections and helper renderers.
JSDoc / Documentation Updates
app/api/accounts/[id]/route.ts, app/api/admins/privy/route.ts, app/api/coding-agent/[platform]/route.ts, app/api/songs/analyze/presets/route.ts, app/api/transcribe/route.ts, many lib/** files
Added/updated @param and related JSDoc annotations across many files; no runtime/signature changes.
Small libs & helpers (JSDoc only)
lib/admins/privy/*, lib/ai/*, lib/catalog/*, lib/chat/*, lib/chats/*, lib/coding-agent/*, lib/composio/*, lib/content/*, lib/credits/*, lib/emails/*, lib/flamingo/*, lib/github/*, lib/mcp/*, lib/prompts/*, lib/spotify/*, lib/supabase/*, lib/transcribe/*, lib/trigger/*
Mostly JSDoc additions documenting parameters and nested fields; functional surface unchanged.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Route as /api/artists/intel
    participant Handler as generateArtistIntelPackHandler
    participant Auth as validateAuthContext
    participant Validator as validateArtistIntelBody
    participant Orchestrator as generateArtistIntelPack
    participant Spotify as Spotify API
    participant Analysis as MusicFlamingo
    participant Perplexity as Perplexity API
    participant TextGen as generateText
    participant Response

    Client->>Route: POST { artist_name }
    Route->>Handler: forward request
    Handler->>Auth: validateAuthContext(request)
    Auth-->>Handler: auth OK / reject
    Handler->>Validator: validateArtistIntelBody(body)
    Validator-->>Handler: validated body
    Handler->>Orchestrator: generateArtistIntelPack(artist_name)

    par Concurrent fetch & analyze
        Orchestrator->>Spotify: getArtistSpotifyData(artist_name)
        Orchestrator->>Analysis: getArtistMusicAnalysis(previewUrl)
        Orchestrator->>Perplexity: getArtistWebContext(artist_name)
        Spotify-->>Orchestrator: artist/topTracks/previewUrl
        Analysis-->>Orchestrator: analysis results
        Perplexity-->>Orchestrator: web context + citations
    end

    Orchestrator->>TextGen: buildArtistMarketingCopy(spotifyData, musicAnalysis, webContext)
    TextGen-->>Orchestrator: marketing copy (JSON)
    Orchestrator-->>Handler: { type: "success", pack } / { type: "error", error }
    Handler->>Response: 200/404 JSON + CORS
    Response-->>Client: Artist Intelligence Pack or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🎧 Docs polished, new packs take flight,

Spotify whispers, AI labs ignite.
Web context fetched, music analysis sings,
Pack forged, formatted—insights on wings.
Tools registered, routes hum through the night.

🚥 Pre-merge checks | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning Pull request violates SOLID principles with unreachable logic in pre-market artist check and lossy rounding in follower gap formatting; excessive function lengths violate SRP. Move pre-market artist condition before efficiency tier checks, extract formatFollowerGap helper, refactor long functions into single-responsibility modules, and consolidate error-handling patterns.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/-u0ajm7x8fbr-implement-the-wil-1774118338794

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/prompts/getSystemPrompt.ts (2)

19-132: 🛠️ Refactor suggestion | 🟠 Major

Refactor to comply with 50-line function limit.

This function spans 113 lines, which violates the coding guideline: "Keep functions under 50 lines" for domain functions in lib/**/*.ts.

Consider decomposing into smaller, focused functions:

  • buildUserContextSection(accountWithDetails, email) for lines 66-109
  • buildArtistInstructionSection(artistInstruction) for lines 111-120
  • buildKnowledgeBaseSection(knowledgeBaseText) for lines 122-129

This would improve testability, readability, and compliance with SRP and the line-count guideline.

As per coding guidelines: "Keep functions under 50 lines" for domain functions in lib/**/*.ts.


67-101: 🛠️ Refactor suggestion | 🟠 Major

Replace "user" terminology with "account" per coding guidelines.

The coding guideline explicitly states: "Use 'account' terminology, never 'entity' or 'user'". This section contains multiple uses of "user":

  • Line 67: "CURRENT USER CONTEXT"
  • Line 70: "the person currently using this application"
  • Line 101: "User's Custom Instructions & Preferences"
📋 Suggested terminology updates
- -----CURRENT USER CONTEXT-----
- This is information about the person currently using this application (the human you're talking to):
+ -----CURRENT ACCOUNT CONTEXT-----
+ This is information about the account currently using this application (the person you're talking to):
- User's Custom Instructions & Preferences:
+ Account's Custom Instructions & Preferences:

As per coding guidelines: "Use 'account' terminology, never 'entity' or 'user'".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/prompts/getSystemPrompt.ts` around lines 67 - 101, In getSystemPrompt.ts
update all human-facing and code identifiers that use "user" to "account":
change the header string "-----CURRENT USER CONTEXT-----" to "-----CURRENT
ACCOUNT CONTEXT-----", change the sentence "the person currently using this
application" to use "account" instead of "user", change "User's Custom
Instructions & Preferences:" to "Account's Custom Instructions & Preferences:",
and rename the variable userSection to accountSection (and update any
references) so strings and identifiers consistently use account; keep
accountWithDetails as-is.
🧹 Nitpick comments (33)
lib/content/getArtistRootPrefix.ts (1)

1-5: Make the JSDoc actionable, not just present.

Good addition, but this block currently only repeats parameter names. Please include short descriptions and return semantics so callers understand fallback behavior quickly.

Suggested JSDoc upgrade
 /**
- *
- * `@param` paths
- * `@param` artistSlug
+ * Selects the root prefix used for an artist's content paths.
+ *
+ * Prefers `artists/{artistSlug}/` when present in `paths`, otherwise falls back
+ * to `{artistSlug}/`, and defaults to `artists/{artistSlug}/` if no match exists.
+ *
+ * `@param` paths Candidate content paths to inspect.
+ * `@param` artistSlug Artist slug used to build candidate prefixes.
+ * `@returns` The chosen root prefix for downstream path construction.
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/getArtistRootPrefix.ts` around lines 1 - 5, Update the JSDoc for
getArtistRootPrefix: add brief descriptions for the parameters (describe what
"paths" contains and what "artistSlug" represents) and document the return value
and fallback behavior (what string is returned on success and what is returned
when no matching path is found). Reference the function name getArtistRootPrefix
and explicitly mention the expected shape or examples for "paths" and any
edge-cases (empty arrays, undefined artistSlug) so callers know how to handle
failures.
lib/content/isCompletedRun.ts (1)

8-11: Replace placeholder JSDoc with meaningful documentation (or remove it).

This block currently adds little value. Please include a short summary plus precise @param/@returns descriptions so the comment improves maintainability.

As per coding guidelines, "KISS: Prefer simple, readable implementations over clever optimizations."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/isCompletedRun.ts` around lines 8 - 11, Replace the placeholder
JSDoc above the isCompletedRun function with a concise summary and precise tags:
add a one-line description (e.g., "Determine whether a Run is completed"), an
`@param` {Run} run describing expected properties used (e.g., status, finishedAt)
and any accepted shapes, and an `@returns` {boolean} explaining true means the run
is completed and under what conditions; ensure the doc matches the
isCompletedRun implementation and types referenced in the function signature.
lib/supabase/song_artists/insertSongArtists.ts (1)

9-10: Complete the @param documentation for clarity.

@param songArtists is currently undocumented beyond the name. Add a short description so generated docs and editor hints are actionable.

Suggested JSDoc update
 /**
  * Inserts song-artist relationships, skipping duplicates.
  *
- * `@param` songArtists
+ * `@param` songArtists Array of song-artist join records to validate, deduplicate, and upsert.
  */

As per coding guidelines, “Supabase database functions should import from @/lib/supabase/serverClient and follow the documented pattern with JSDoc comments, error handling, and return types.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/supabase/song_artists/insertSongArtists.ts` around lines 9 - 10, Update
the JSDoc for the insertSongArtists function by adding a clear description for
the `@param` songArtists entry (e.g., "array of song-artist relationship objects
to insert, each with song_id and artist_id"), and ensure the comment includes
the expected shape of each item; keep the existing JSDoc tags for return and
throws intact and follow the Supabase pattern used in this file (import from
`@/lib/supabase/serverClient` and proper error handling in insertSongArtists) so
editor/docs show the parameter shape and intent.
lib/content/getArtistFileTree.ts (2)

7-9: Clarify JSDoc parameter intent.

Good addition of @param tags, but these should include short descriptions (and expected format) so IDE/tooling hints are actually useful, e.g. repo URL/name format and slug format.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/getArtistFileTree.ts` around lines 7 - 9, Update the JSDoc for
the getArtistFileTree function to add short descriptions and expected formats
for the `@param` tags: describe githubRepo (e.g., repository owner/name or full
GitHub URL) and artistSlug (e.g., URL-safe lowercase identifier, permitted
characters), so IDE/tooling can show the intended input shape; keep descriptions
concise and consistent with existing project conventions.

17-20: Extract duplicate artist-path matching logic into a helper.

The blobPaths + hasArtist computation is repeated in two branches. Pulling this into a small local utility would reduce duplication and keep the function easier to evolve.

As per coding guidelines, "DRY: Consolidate similar logic into shared utilities".

Also applies to: 28-31

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/getArtistFileTree.ts` around lines 17 - 20, Extract the repeated
blob path / artist-match logic inside getArtistFileTree into a small local
helper (e.g., hasArtistInTree or getBlobPathsAndHasArtist) that accepts the tree
array (mainTree) and artistSlug and returns the blobPaths and/or boolean
hasArtist; replace both duplicated blocks that compute blobPaths and hasArtist
with calls to this helper so both branches reuse the same logic and avoid
duplication.
lib/ai/isEmbedModel.ts (2)

6-7: Complete the @param annotation with a description.

The @param m tag is missing a description. JSDoc best practice requires parameter documentation to include both the name and a description of what the parameter represents.

📝 Proposed fix to complete the documentation
 *
-* `@param` m
+* `@param` m - The gateway language model entry to check
 */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ai/isEmbedModel.ts` around lines 6 - 7, The JSDoc for the isEmbedModel
function is missing a description for the `@param` m tag; update the JSDoc above
the isEmbedModel function (reference symbol: isEmbedModel) to include a brief
description of parameter m (e.g., "the model identifier or object to check") so
the `@param` line documents what m represents and its expected type/format.

16-16: Remove redundant default export.

The function is already exported as a named export on line 9. Having both named and default exports of the same function creates ambiguity about the preferred import pattern and adds unnecessary complexity.

♻️ Proposed fix to simplify exports
-
-export default isEmbedModel;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ai/isEmbedModel.ts` at line 16, Remove the redundant default export for
isEmbedModel: keep the existing named export (export function isEmbedModel ...)
and delete the trailing "export default isEmbedModel;" statement; then search
for any consumers importing the default and update them to the named import {
isEmbedModel } to ensure a single, unambiguous export pattern.
lib/trigger/triggerCreateContent.ts (1)

19-20: Enhance the JSDoc parameter documentation.

The @param tag was added but lacks a description. Consider adding a brief explanation of what the payload contains, and include a @returns tag to document the returned handle.

📝 Suggested JSDoc enhancement
 /**
  * Triggers the create-content task in Trigger.dev.
  *
- * `@param` payload
+ * `@param` payload - The content creation parameters including account, artist, template, and generation options.
+ * `@returns` A handle to the triggered task that can be used to track execution.
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/trigger/triggerCreateContent.ts` around lines 19 - 20, Update the JSDoc
for the triggerCreateContent function: add a brief description for the payload
parameter (what fields it contains and their purpose, e.g., content data,
metadata, userId) and add a `@returns` tag describing the handle returned (type
and what it represents, e.g., Promise<Handle> or created content ID/URL). Ensure
the tags reference the existing parameter name "payload" and the function
"triggerCreateContent" so maintainers can quickly understand inputs and outputs.
lib/chat/toolChains/getPrepareStepResult.ts (1)

15-16: Nice documentation addition!

Adding the @param tag improves the JSDoc completeness. For even better developer experience, consider adding a brief description to explain the parameter's purpose.

📝 Optional enhancement for more descriptive JSDoc
  *
- * `@param` options
+ * `@param` options - Configuration containing steps, model, messages, and step number for determining the next tool in the chain
  */

Alternatively, you could document the individual properties if you want more detail:

  *
- * `@param` options
+ * `@param` options - The step preparation options
+ * `@param` options.steps - Array of previous step results with tool executions
+ * `@param` options.stepNumber - Current step number in the conversation
+ * `@param` options.model - The language model being used
+ * `@param` options.messages - Array of conversation messages
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/chat/toolChains/getPrepareStepResult.ts` around lines 15 - 16, Update the
JSDoc for getPrepareStepResult to add a brief description for the `@param` options
entry (and optionally document its key properties) so callers understand what
the options object controls; locate the JSDoc above the getPrepareStepResult
function and add one-line text describing the purpose of options (e.g., what
values it holds and how it affects the function), and if helpful, add short
`@param` entries for specific properties of options referenced in the function.
lib/flamingo/getFlamingoPresetsHandler.ts (1)

13-13: Complete the JSDoc @param annotation with a description.

The @param request tag is missing a description. JSDoc best practice is to include a brief description after the parameter name to provide context in IDE tooltips and generated documentation.

📝 Suggested fix for complete documentation
- * `@param` request
+ * `@param` request - The incoming Next.js HTTP request
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/flamingo/getFlamingoPresetsHandler.ts` at line 13, Add a brief
descriptive text to the JSDoc `@param request` tag for the
getFlamingoPresetsHandler function so IDE tooltips and generated docs show
context; locate the JSDoc block above the getFlamingoPresetsHandler function and
change the `@param request` line to include a short description of what
`request` represents (e.g., incoming HTTP request or handler input) and any
important details such as expected properties or type.
lib/coding-agent/parseMergeActionId.ts (1)

4-11: Complete the JSDoc documentation and add explicit return type.

The @param annotation was added but lacks a description. Additionally, the function is missing a @returns tag and an explicit return type annotation. While TypeScript can infer the return type, explicitly declaring it improves both documentation and type safety.

📝 Proposed enhancements for documentation completeness
 /**
  * Parses a merge action ID like "merge_pr:recoupable/api#42"
  * into { repo, number } or null if the format doesn't match.
  *
- * `@param` actionId
+ * `@param` actionId - The action ID string to parse
+ * `@returns` An object with repo and PR number, or null if format is invalid
  */
-export function parseMergeActionId(actionId: string) {
+export function parseMergeActionId(
+  actionId: string
+): { repo: string; number: number } | null {
   const match = actionId.match(/^merge_pr:(.+)#(\d+)$/);
   if (!match) return null;
   return { repo: match[1], number: parseInt(match[2], 10) };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/coding-agent/parseMergeActionId.ts` around lines 4 - 11, Update the JSDoc
for parseMergeActionId to document the parameter and return value and add an
explicit return type on the function signature: describe the actionId parameter
(e.g., "actionId - string in the form 'merge_pr:<repo>#<number>'"), add a
`@returns` tag explaining that it returns an object with repo and number or null
on no match, and change the signature of parseMergeActionId(actionId: string) to
explicitly return { repo: string; number: number } | null.
lib/coding-agent/parseMergeTestToMainActionId.ts (1)

4-12: Complete the JSDoc documentation and add explicit return type.

The @param annotation was added but lacks a description. Additionally, the function is missing a @returns tag and an explicit return type annotation, which would improve both documentation clarity and TypeScript type safety.

📝 Proposed enhancements for documentation completeness
 /**
  * Parses a merge_test_to_main action ID like "merge_test_to_main:recoupable/api"
  * into the repo string, or null if the format doesn't match.
  *
- * `@param` actionId
+ * `@param` actionId - The action ID string to parse
+ * `@returns` The repository string (e.g., "owner/repo") or null if format is invalid
  */
-export function parseMergeTestToMainActionId(actionId: string): string | null {
+export function parseMergeTestToMainActionId(actionId: string): string | null {

Note: The explicit return type annotation is already present, so only the JSDoc enhancements are needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/coding-agent/parseMergeTestToMainActionId.ts` around lines 4 - 12, Update
the JSDoc for parseMergeTestToMainActionId to describe the parameter and return
value: add a brief description for `@param` actionId (e.g., "the action identifier
to parse, expected to start with 'merge_test_to_main:'") and add an `@returns` tag
that explains the function returns the repository string in "owner/name" format
or null if the prefix is missing or the repo is malformed; keep the existing
explicit return type string | null and ensure the top-level description
summarizes what the function does.
lib/catalog/getCatalogs.ts (1)

11-14: Complete the JSDoc parameter description.

The @param accountId tag lacks a description. Document what account ID is used for to provide clarity.

📝 Suggested JSDoc improvement
 /**
- *
- * `@param` accountId
+ * Fetches all catalogs for a given account
+ *
+ * `@param` accountId - The unique identifier of the account whose catalogs to fetch
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/catalog/getCatalogs.ts` around lines 11 - 14, Update the JSDoc for the
getCatalogs function to describe the accountId parameter: replace the bare
"@param accountId" with a short description such as "the unique identifier of
the account whose catalogs should be retrieved" (or similarly precise wording)
so readers know which account the function will query; ensure the description
mentions expected format (e.g., numeric or UUID) if applicable and keep it
adjacent to the getCatalogs function declaration.
lib/catalog/formatCatalogSongsAsCSV.ts (1)

5-6: Complete the JSDoc parameter description.

The @param songs tag lacks a description. JSDoc parameters should explain what the parameter represents and its purpose to be useful for developers.

📝 Suggested JSDoc improvement
 /**
  * Formats catalog songs into the CSV-like format expected by the scorer
  *
- * `@param` songs
+ * `@param` songs - Array of catalog songs to format as CSV
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/catalog/formatCatalogSongsAsCSV.ts` around lines 5 - 6, The JSDoc for the
function formatCatalogSongsAsCSV is missing a description for the `@param` songs
tag; update the JSDoc to describe what songs is (e.g., an array of song
objects), what each element contains (fields used by the function such as title,
artist, album, duration, etc.), and any expectations (non-empty array, optional
fields), so maintainers know the input shape and constraints expected by
formatCatalogSongsAsCSV; reference the function name formatCatalogSongsAsCSV and
the parameter name songs when adding the description.
lib/catalog/getCatalogSongs.ts (1)

28-34: Complete the JSDoc parameter descriptions.

All four @param tags lack descriptions. Effective documentation should explain each parameter's purpose, especially for optional parameters like artistName and parameters with default values.

📝 Suggested JSDoc improvement
 /**
- *
- * `@param` catalogId
- * `@param` pageSize
- * `@param` page
- * `@param` artistName
+ * Fetches a single page of songs from a catalog
+ *
+ * `@param` catalogId - The unique identifier of the catalog
+ * `@param` pageSize - Number of songs per page (default: 100)
+ * `@param` page - Page number to fetch (default: 1)
+ * `@param` artistName - Optional filter to fetch songs by a specific artist
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/catalog/getCatalogSongs.ts` around lines 28 - 34, Update the JSDoc for
getCatalogSongs to provide meaningful descriptions for each `@param`: describe
catalogId as the identifier of the catalog to query (string), pageSize as the
number of songs to return per page (number, default value if applicable), page
as the 1-based page index to fetch (number, default if applicable), and
artistName as an optional filter to limit results to a specific artist (string,
optional). Mention default values and optionality where applicable so callers
understand behavior and which parameters can be omitted.
lib/catalog/getCatalogDataAsCSV.ts (1)

6-7: Complete the JSDoc parameter description.

The @param catalogId tag lacks a description. Document what the catalog ID represents to help developers understand the function's requirements.

📝 Suggested JSDoc improvement
 /**
  * Gets all catalog songs and formats them as CSV for the scorer
  *
- * `@param` catalogId
+ * `@param` catalogId - The unique identifier of the catalog to fetch and format
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/catalog/getCatalogDataAsCSV.ts` around lines 6 - 7, Add a clear JSDoc
description for the catalogId parameter in the getCatalogDataAsCSV function:
explain that catalogId is the unique identifier (string or numeric ID) of the
catalog whose data will be exported to CSV, whether it must be a UUID or
database primary key, and note any validation/format expectations (e.g.,
non-empty string). Update the `@param` tag for catalogId in the
getCatalogDataAsCSV JSDoc accordingly so callers know what value to pass.
lib/supabase/storage/uploadFileByKey.ts (1)

6-11: Enhance JSDoc with types and descriptions.

The @param entries list parameter names but lack type information and descriptions, which reduces their utility for developers and IDE intellisense. Standard JSDoc format includes both:

`@param` {string} key - The storage key/path for the uploaded file
`@param` {File | Blob} file - The file content to upload
`@param` {object} [options] - Upload options
`@param` {string} [options.contentType] - MIME type of the file (defaults to "application/octet-stream")
`@param` {boolean} [options.upsert] - Whether to overwrite existing files (defaults to false)
📝 Proposed JSDoc enhancement
 /**
  * Upload file to Supabase storage by key
  *
- * `@param` key
- * `@param` file
- * `@param` options
- * `@param` options.contentType
- * `@param` options.upsert
+ * `@param` {string} key - The storage key/path for the uploaded file
+ * `@param` {File | Blob} file - The file content to upload
+ * `@param` {object} [options] - Upload options
+ * `@param` {string} [options.contentType] - MIME type of the file (defaults to "application/octet-stream")
+ * `@param` {boolean} [options.upsert] - Whether to overwrite existing files (defaults to false)
+ * `@returns` {Promise<void>}
+ * `@throws` {Error} When upload fails
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/supabase/storage/uploadFileByKey.ts` around lines 6 - 11, Update the
JSDoc for the uploadFileByKey function to include types and short descriptions
for each parameter: document key as `@param` {string} key - The storage key/path
for the uploaded file; file as `@param` {File|Blob} file - The file content to
upload; options as `@param` {object} [options] - Upload options;
options.contentType as `@param` {string} [options.contentType] - MIME type of the
file (defaults to "application/octet-stream"); and options.upsert as `@param`
{boolean} [options.upsert] - Whether to overwrite existing files (defaults to
false); keep the JSDoc block above the uploadFileByKey declaration so IDEs can
pick up the types and defaults.
lib/spotify/getSpotifyFollowers.ts (1)

57-61: Consider documenting the first-result assumption.

The function returns follower count from the first search result without verifying it's the intended artist. For artists with common names or similar-sounding artists, this could return data for the wrong artist. Consider either:

  • Adding a JSDoc note that the function returns the most relevant match
  • Or adding validation logic to improve match confidence
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/spotify/getSpotifyFollowers.ts` around lines 57 - 61, The
getSpotifyFollowers function currently returns followers from
data.artists.items[0] without ensuring that item is the intended artist; update
the code or docs: either add a JSDoc comment on getSpotifyFollowers stating it
returns the most relevant (first) search match, or implement simple validation
before returning (e.g., compare artistName normalized/case-insensitive against
data.artists.items[0].name, optionally fall back to searching items for an exact
match or returning null/error if no confident match). Reference
data.artists.items[0] and the getSpotifyFollowers function when making the
change.
lib/content/getArtistContentReadiness.ts (2)

78-126: Optional: Consolidate file checks into a declarative structure.

The six file-checking blocks follow a repetitive pattern. Consider refactoring to a data-driven approach for improved maintainability:

♻️ Example declarative refactor
const fileChecks = [
  {
    check: () => hasFile("context/images/face-guide.png"),
    file: "context/images/face-guide.png",
    severity: "required" as const,
    fix: "Generate a face guide image before creating content.",
  },
  {
    check: () => hasFile("config/content-creation/config.json"),
    file: "config/content-creation/config.json",
    severity: "recommended" as const,
    fix: "Add a pipeline config to override default model/resolution settings.",
  },
  {
    check: () => hasAnyMp3,
    file: "songs/*.mp3",
    severity: "required" as const,
    fix: "Add at least one .mp3 file for audio selection.",
  },
  // ... remaining checks
];

const issues = fileChecks
  .filter(check => !check.check())
  .map(({ file, severity, fix }) => ({ file, severity, fix }));

This approach reduces duplication, makes adding new file checks trivial, and aligns with the DRY principle.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/getArtistContentReadiness.ts` around lines 78 - 126, The repeated
if-blocks performing file presence checks (using hasFile and hasAnyMp3) should
be replaced with a declarative array of check descriptors and a single
filter/map pipeline to populate the issues array; create a fileChecks array of
objects each containing a check function (or boolean), file, severity, and fix,
then set issues = fileChecks.filter(c => !c.check()).map(({file,severity,fix})
=> ({file,severity,fix})); update references to hasFile and hasAnyMp3 inside
those descriptors and remove the duplicated if blocks so adding or changing
checks is just editing the fileChecks entries.

31-138: Consider splitting this function to meet the 50-line guideline.

The getArtistContentReadiness function currently spans ~107 lines, exceeding the coding guideline of "Keep functions under 50 lines." While the logic is clear, breaking it into smaller, focused functions would improve maintainability and testability.

Consider extracting:

  • File tree validation logic into a separate function
  • The file-checking rules into a data-driven configuration
  • Issue categorization and result building into helper functions

This refactor would align with both the 50-line guideline and Single Responsibility Principle at the function level. As per coding guidelines, "Keep functions under 50 lines" for domain functions in lib/**/*.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/content/getArtistContentReadiness.ts` around lines 31 - 138, Split
getArtistContentReadiness into smaller helpers: extract the repo/tree validation
into a function (e.g., validateRepoAndTree that calls selectAccountSnapshots and
getArtistFileTree and returns githubRepo and blobPaths or early-not-ready
response), move path-check logic into a helper that builds issues from a
data-driven list (e.g., buildFileChecks or collectIssues that uses
getArtistRootPrefix and implements hasFile/hasAnyMp3 checks), and extract the
final categorization/response construction into a buildReadinessResult helper
that computes requiredMissing, warnings and returns the final object; keep
getArtistContentReadiness as an orchestration function that calls
validateRepoAndTree, collectIssues, and buildReadinessResult so each function
stays under ~50 lines and names like getArtistContentReadiness,
validateRepoAndTree, collectIssues/buildFileChecks, and buildReadinessResult
appear in the diff for easy location.
lib/prompts/getSystemPrompt.ts (1)

40-129: Consider using array-based string composition for cleaner code.

The current pattern of systemPrompt = \${systemPrompt}${section}`` works but could be simplified. Consider building sections in an array and joining them, which can improve readability and make testing individual sections easier.

💡 Alternative approach
const sections = [SYSTEM_PROMPT + /* base prompt content */];

if (accountWithDetails) {
  sections.push(buildUserContextSection(accountWithDetails, email));
}

if (artistInstruction) {
  sections.push(buildArtistInstructionSection(artistInstruction));
}

if (knowledgeBaseText) {
  sections.push(buildKnowledgeBaseSection(knowledgeBaseText));
}

return sections.join('\n');

This pairs well with the earlier suggestion to extract section-building logic into separate functions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/prompts/getSystemPrompt.ts` around lines 40 - 129, The prompt-building
code repeatedly mutates systemPrompt by concatenating sections; refactor to
build an array of sections and join them to improve readability and testability:
create a sections array seeded with SYSTEM_PROMPT and the IMAGE
EDITING/IMPORTANT CONTEXT block, then replace the repeated `systemPrompt =
\`${systemPrompt}${...}\`` logic by pushing sections for accountWithDetails (use
a helper like buildUserContextSection to assemble the userSection using
accountWithDetails and email), artistInstruction
(buildArtistInstructionSection), and knowledgeBaseText
(buildKnowledgeBaseSection), and finally set systemPrompt = sections.join('\n');
ensure existing identifiers systemPrompt, accountWithDetails, artistInstruction,
knowledgeBaseText, and the userSection construction are used to locate and
replace the current concatenation logic.
lib/admins/privy/countNewAccounts.ts (1)

9-10: Align parameter docs with project terminology.

Please avoid users in docs and prefer accounts (or a specific subtype) to match repo language conventions.

As per coding guidelines "**/*.{ts,tsx}: Use 'account' terminology, never 'entity' or 'user'; use specific names like 'artist', 'workspace', 'organization' when referring to specific types".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/admins/privy/countNewAccounts.ts` around lines 9 - 10, Update the JSDoc
param name to use the project's preferred terminology: replace the `@param` tag
that currently says "users" with "accounts" (or a more specific subtype if
applicable) in the documentation for the countNewAccounts function so the docs
match the repo convention; ensure any inline references in the comment block to
"users" are renamed to "accounts" (or e.g., "artist", "workspace",
"organization" if that more accurately describes the input) while leaving the
function and implementation unchanged.
app/api/accounts/[id]/route.ts (1)

26-27: Clarify destructured params in JSDoc.

@param params.params is ambiguous. Prefer documenting the destructured argument once (e.g., @param context - Route context containing params.id) to avoid confusion.

Suggested doc cleanup
- * `@param` params.params
- * `@param` params - Route params containing the account ID
+ * `@param` context - Route context containing params.id (account UUID)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/accounts/`[id]/route.ts around lines 26 - 27, Update the JSDoc for
the route handler to remove the ambiguous "@param params.params" and document
the destructured parameter once; e.g., replace the two entries with a single
line like "@param context - Route context containing params.id (the account ID)"
and, if the handler destructures params (e.g., function handler({ params }) or
function GET(context)), mention that params.id is the account identifier so
readers know which property is used (reference params and params.id in the
comment).
lib/admins/privy/fetchPrivyLogins.ts (1)

23-26: Consolidate duplicated JSDoc into a single function-level block.

This new placeholder doc is too thin and, combined with the earlier block, leaves docs split across the type alias and function. Prefer one complete JSDoc directly above fetchPrivyLogins for clarity and DRYness.

♻️ Suggested doc cleanup
-/**
- * Fetches Privy users active or created within the given period via the Privy Management API.
- * Returns the full, unmodified user objects from Privy.
- * Paginates through all users, collecting those whose created_at or latest_verified_at
- * falls within the time window.
- *
- * `@see` https://docs.privy.io/api-reference/users/get-all
- * `@param` period - "daily", "weekly", or "monthly"
- * `@returns` Array of full Privy user objects within the time window, sorted by created_at descending
- */
 export type FetchPrivyLoginsResult = {
   users: User[];
   totalPrivyUsers: number;
 };
 
 /**
- *
- * `@param` period
+ * Fetches Privy users active or created within the given period via the Privy Management API.
+ * Returns full user objects from Privy and total user count seen during pagination.
+ *
+ * `@see` https://docs.privy.io/api-reference/users/get-all
+ * `@param` period - "all", "daily", "weekly", or "monthly"
+ * `@returns` Users in window (sorted by created_at desc) and totalPrivyUsers
  */
 export async function fetchPrivyLogins(period: PrivyLoginsPeriod): Promise<FetchPrivyLoginsResult> {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/admins/privy/fetchPrivyLogins.ts` around lines 23 - 26, Consolidate the
duplicated JSDoc by removing the thin/placeholder block currently above the type
alias and create one complete JSDoc directly above the fetchPrivyLogins function
describing its parameters (e.g., period), return value, and behavior; ensure the
JSDoc fully documents fetchPrivyLogins (and remove the separate doc on the type
alias) so all documentation is single-source and immediately precedes the
fetchPrivyLogins declaration.
lib/artistIntel/getArtistMusicAnalysis.ts (1)

29-40: Generation parameters are overridden by preset config.

Per processAnalyzeMusicRequest implementation, when a preset is provided, the preset's own parameters (max_new_tokens, temperature, do_sample) are extracted from the preset config and override any values passed here. The top_p is also set to undefined for presets.

These hardcoded values (lines 34-37) are effectively dead code and may mislead future maintainers into thinking they control the generation behavior. Consider removing them for clarity:

♻️ Suggested simplification
     ANALYSIS_PRESETS.map(preset =>
       processAnalyzeMusicRequest({
         preset,
         audio_url: previewUrl,
-        max_new_tokens: 512,
-        temperature: 1.0,
-        top_p: 1.0,
-        do_sample: false,
       }),
     ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/getArtistMusicAnalysis.ts` around lines 29 - 40, The
generation params passed into processAnalyzeMusicRequest are shadowed by the
provided preset, so remove the dead hardcoded options (max_new_tokens,
temperature, top_p, do_sample) from the ANALYSIS_PRESETS.map call and only pass
the preset and audio_url (and any truly dynamic args); update the map invocation
that calls processAnalyzeMusicRequest(preset, audio_url, ...) to call
processAnalyzeMusicRequest({ preset, audio_url }) so the preset-driven config is
used and future maintainers aren’t misled by ineffective overrides.
app/api/admins/privy/route.ts (1)

18-20: Empty JSDoc for OPTIONS handler.

The OPTIONS handler should document its CORS preflight purpose:

📝 Suggested improvement
 /**
- *
+ * OPTIONS /api/admins/privy
+ *
+ * CORS preflight handler returning appropriate headers.
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/admins/privy/route.ts` around lines 18 - 20, The empty JSDoc above
the OPTIONS handler should be replaced with a short descriptive comment that
documents this handler's role as the CORS preflight responder: update the JSDoc
for the OPTIONS function (the OPTIONS handler in app/api/admins/privy/route.ts)
to state it handles CORS preflight requests, lists the allowed methods and
headers it responds with (e.g., Access-Control-Allow-Methods,
Access-Control-Allow-Headers, Access-Control-Allow-Origin), and notes it returns
a 204/OK empty response for preflight checks so future readers understand its
purpose.
app/api/transcribe/route.ts (1)

5-8: Empty JSDoc provides no value.

The JSDoc block has an empty description and @param req without any explanation. Per the coding guidelines, API routes should have meaningful JSDoc comments that describe the endpoint's purpose, expected inputs, and responses.

Consider documenting what this endpoint does:

📝 Suggested improvement
 /**
- *
- * `@param` req
+ * POST /api/transcribe
+ *
+ * Transcribes audio files using OpenAI Whisper and saves both the original
+ * audio and transcript markdown to the artist's files.
+ *
+ * `@param` req - Request containing audio_url, account_id, artist_account_id, title, and include_timestamps.
+ * `@returns` Transcription result with audio file, transcript file, text, and language.
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/transcribe/route.ts` around lines 5 - 8, Replace the empty JSDoc
above the API route handler with a meaningful comment: describe the endpoint's
purpose, the expected request shape (e.g., multipart/form-data or JSON,
file/audio format, and any required fields in req), the returned response
structure and status codes, and document the req parameter (e.g., Request) and
what it should contain; update the JSDoc immediately above the route handler
function (the POST handler/exported route in route.ts) to include these details
and remove the blank `@param` entry so the comment provides clear API usage and
expectations.
lib/mcp/resolveAccountId.ts (1)

18-21: JSDoc param annotations lack descriptions.

The nested @param tags would be more helpful with brief descriptions:

📝 Suggested improvement
  * `@param` params - The auth info and optional account_id override.
- * `@param` params.authInfo
- * `@param` params.accountIdOverride
+ * `@param` params.authInfo - MCP authentication info from the request.
+ * `@param` params.accountIdOverride - Optional account_id to use instead of authenticated account.
  * `@returns` The resolved accountId or an error message.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mcp/resolveAccountId.ts` around lines 18 - 21, Update the JSDoc for
resolveAccountId to add brief descriptions for the nested parameters: describe
params as the input object containing auth info and optional override, document
params.authInfo (e.g., the authentication object containing user/org identifiers
or tokens used for resolution), and document params.accountIdOverride (e.g.,
optional string to force a specific accountId). Also ensure the `@returns` line
briefly explains the resolved accountId or an error message format so callers
know what to expect.
lib/mcp/tools/transcribe/registerTranscribeAudioTool.ts (1)

18-21: Empty JSDoc provides no documentation value.

The JSDoc block has an empty description and @param server without explanation. Consider providing meaningful documentation:

📝 Suggested improvement
 /**
- *
- * `@param` server
+ * Registers the `transcribe_audio` MCP tool for audio transcription via OpenAI Whisper.
+ *
+ * `@param` server - The MCP server instance to register the tool on.
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mcp/tools/transcribe/registerTranscribeAudioTool.ts` around lines 18 -
21, The JSDoc above the registerTranscribeAudioTool function is empty and
unhelpful; replace it with a concise description of what
registerTranscribeAudioTool does and document the parameter `server` (its type
or expected interface and role) and any return value or side effects; update the
comment block near the registerTranscribeAudioTool declaration in
registerTranscribeAudioTool.ts to include a one-line summary, `@param server`
explanation, and `@returns` or `@throws` if applicable so IDEs and readers get
meaningful documentation.
lib/artistIntel/buildArtistMarketingCopy.ts (2)

63-66: Consider validating AI response structure with Zod.

The JSON.parse(result.text) result is cast directly to ArtistMarketingCopy without runtime validation. AI models can return malformed JSON, missing fields, or unexpected structures. Adding Zod validation would provide type safety and clearer error handling:

♻️ Suggested improvement
+import { z } from "zod";
+
+const ArtistMarketingCopySchema = z.object({
+  playlist_pitch_email: z.string(),
+  instagram_caption: z.string(),
+  tiktok_caption: z.string(),
+  twitter_post: z.string(),
+  press_release_opener: z.string(),
+  key_talking_points: z.array(z.string()),
+});
+
 // In the try block:
   try {
     const result = await generateText({ system: SYSTEM_PROMPT, prompt });
-    const parsed = JSON.parse(result.text) as ArtistMarketingCopy;
-    return parsed;
+    const parsed = ArtistMarketingCopySchema.parse(JSON.parse(result.text));
+    return parsed;
   } catch (error) {

This aligns with the codebase pattern of using Zod for input/output validation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/buildArtistMarketingCopy.ts` around lines 63 - 66, The code
in buildArtistMarketingCopy calls generateText and directly casts
JSON.parse(result.text) to ArtistMarketingCopy; add runtime validation using a
Zod schema (e.g., define/import ArtistMarketingCopySchema) to parse and validate
the parsed object instead of a direct cast, catching Zod or JSON errors and
returning/throwing a clear error with context (include result.text) from the
buildArtistMarketingCopy function so malformed AI responses are detected and
handled safely.

34-35: Guard against empty topTracks array.

If topTracks is an empty array, topTracks[0] will be undefined. While the optional chaining on topTrack?.name handles this downstream, adding an explicit check improves readability:

♻️ Minor clarity improvement
   const { artist, topTracks } = spotifyData;
-  const topTrack = topTracks[0];
+  const topTrack = topTracks.length > 0 ? topTracks[0] : undefined;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/buildArtistMarketingCopy.ts` around lines 34 - 35, The code
grabs topTracks from spotifyData and directly indexes topTracks[0] into topTrack
which can be undefined when topTracks is an empty array; update the
buildArtistMarketingCopy logic to explicitly guard for an empty topTracks (e.g.,
compute topTrack = Array.isArray(topTracks) && topTracks.length > 0 ?
topTracks[0] : null) and use that guarded value wherever topTrack is referenced
so downstream optional chaining remains clear and the intent is explicit
(referencing symbols: spotifyData, topTracks, topTrack,
buildArtistMarketingCopy).
lib/mcp/tools/artistIntel/registerGenerateArtistIntelPackTool.ts (1)

11-16: Share the artist_name schema with API validation to prevent drift.

This schema duplicates lib/artistIntel/validateArtistIntelBody.ts; any future change can desync MCP vs API behavior.

♻️ Suggested refactor
-const toolSchema = z.object({
-  artist_name: z
-    .string()
-    .min(1)
-    .describe("The artist name to analyze (e.g. 'Taylor Swift', 'Bad Bunny', 'Olivia Rodrigo')."),
-});
+import { artistIntelBodySchema } from "@/lib/artistIntel/artistIntelSchema";
+
+const toolSchema = artistIntelBodySchema;

Create lib/artistIntel/artistIntelSchema.ts as the shared source of truth, then import it in both MCP and API validators.

As per coding guidelines **/*.{ts,tsx}: Extract shared logic into reusable utilities following Don't Repeat Yourself (DRY) principle.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/mcp/tools/artistIntel/registerGenerateArtistIntelPackTool.ts` around
lines 11 - 16, The toolSchema definition duplicates the API validator; extract
the shared schema into a new module (e.g., export a zod schema from
lib/artistIntel/artistIntelSchema.ts) and replace the local const toolSchema
here with an import of that shared schema (preserving the artist_name field
constraints), and update lib/artistIntel/validateArtistIntelBody.ts to import
the same symbol so both MCP (registerGenerateArtistIntelPackTool -> toolSchema /
artist_name) and the API use the single source of truth.
lib/artistIntel/getArtistSpotifyData.ts (1)

5-23: Use shared Spotify types instead of local duplicates.

These local interfaces will drift from the canonical Spotify types and duplicate maintenance effort.

As per coding guidelines **/*.{ts,tsx}: Extract shared logic into reusable utilities following Don't Repeat Yourself (DRY) principle.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/getArtistSpotifyData.ts` around lines 5 - 23, The file
defines local duplicate interfaces SpotifyArtist and SpotifyTrack which should
be replaced with the project's shared Spotify types to avoid drift; remove these
local interfaces from getArtistSpotifyData.ts and import the canonical types
(e.g., SpotifyArtist, SpotifyTrack or equivalent) from the shared types module
used across the repo (check modules like types/spotify, shared/types, or
src/types) and update any references in getArtistSpotifyData and related
functions to use the imported types so the code relies on the single source of
truth.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4a95954a-95fd-4f4b-8f6b-555ee862cce5

📥 Commits

Reviewing files that changed from the base of the PR and between 1b06393 and 57cf714.

⛔ Files ignored due to path filters (24)
  • lib/admins/emails/__tests__/validateGetAdminEmailsQuery.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/artistIntel/__tests__/generateArtistIntelPack.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/artists/__tests__/createArtistPostHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/artists/__tests__/validateCreateArtistBody.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/auth/__tests__/validateAuthContext.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/chat/__tests__/integration/chatEndToEnd.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/coding-agent/__tests__/handleGitHubWebhook.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/coding-agent/__tests__/onMergeTestToMainAction.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/content/__tests__/validateCreateContentBody.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/evals/callChatFunctions.ts is excluded by !**/evals/** and included by lib/**
  • lib/evals/callChatFunctionsWithResult.ts is excluded by !**/evals/** and included by lib/**
  • lib/evals/createToolsCalledScorer.ts is excluded by !**/evals/** and included by lib/**
  • lib/evals/extractTextFromResult.ts is excluded by !**/evals/** and included by lib/**
  • lib/evals/extractTextResultFromSteps.ts is excluded by !**/evals/** and included by lib/**
  • lib/evals/getCatalogSongsCountExpected.ts is excluded by !**/evals/** and included by lib/**
  • lib/evals/getSpotifyFollowersExpected.ts is excluded by !**/evals/** and included by lib/**
  • lib/evals/scorers/CatalogAvailability.ts is excluded by !**/evals/** and included by lib/**
  • lib/evals/scorers/QuestionAnswered.ts is excluded by !**/evals/** and included by lib/**
  • lib/evals/scorers/ToolsCalled.ts is excluded by !**/evals/** and included by lib/**
  • lib/flamingo/__tests__/getFlamingoPresetsHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/notifications/__tests__/createNotificationHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/notifications/__tests__/validateCreateNotificationBody.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/tasks/__tests__/getTaskRunHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/tasks/__tests__/validateGetTaskRunQuery.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (66)
  • app/api/accounts/[id]/route.ts
  • app/api/admins/privy/route.ts
  • app/api/artists/intel/route.ts
  • app/api/coding-agent/[platform]/route.ts
  • app/api/songs/analyze/presets/route.ts
  • app/api/transcribe/route.ts
  • lib/admins/privy/countNewAccounts.ts
  • lib/admins/privy/fetchPrivyLogins.ts
  • lib/admins/privy/getCutoffMs.ts
  • lib/admins/privy/getLatestVerifiedAt.ts
  • lib/admins/privy/toMs.ts
  • lib/ai/getModel.ts
  • lib/ai/isEmbedModel.ts
  • lib/artistIntel/buildArtistMarketingCopy.ts
  • lib/artistIntel/generateArtistIntelPack.ts
  • lib/artistIntel/generateArtistIntelPackHandler.ts
  • lib/artistIntel/getArtistMusicAnalysis.ts
  • lib/artistIntel/getArtistSpotifyData.ts
  • lib/artistIntel/getArtistWebContext.ts
  • lib/artistIntel/validateArtistIntelBody.ts
  • lib/catalog/formatCatalogSongsAsCSV.ts
  • lib/catalog/getCatalogDataAsCSV.ts
  • lib/catalog/getCatalogSongs.ts
  • lib/catalog/getCatalogs.ts
  • lib/chat/toolChains/getPrepareStepResult.ts
  • lib/chats/processCompactChatRequest.ts
  • lib/coding-agent/encodeGitHubThreadId.ts
  • lib/coding-agent/handleMergeSuccess.ts
  • lib/coding-agent/parseMergeActionId.ts
  • lib/coding-agent/parseMergeTestToMainActionId.ts
  • lib/composio/getCallbackUrl.ts
  • lib/content/contentTemplates.ts
  • lib/content/createContentHandler.ts
  • lib/content/getArtistContentReadiness.ts
  • lib/content/getArtistFileTree.ts
  • lib/content/getArtistRootPrefix.ts
  • lib/content/getContentValidateHandler.ts
  • lib/content/isCompletedRun.ts
  • lib/content/persistCreateContentRunVideo.ts
  • lib/content/validateCreateContentBody.ts
  • lib/content/validateGetContentEstimateQuery.ts
  • lib/content/validateGetContentValidateQuery.ts
  • lib/credits/getCreditUsage.ts
  • lib/credits/handleChatCredits.ts
  • lib/emails/processAndSendEmail.ts
  • lib/flamingo/getFlamingoPresetsHandler.ts
  • lib/github/expandSubmoduleEntries.ts
  • lib/github/getRepoGitModules.ts
  • lib/github/resolveSubmodulePath.ts
  • lib/mcp/resolveAccountId.ts
  • lib/mcp/tools/artistIntel/index.ts
  • lib/mcp/tools/artistIntel/registerGenerateArtistIntelPackTool.ts
  • lib/mcp/tools/index.ts
  • lib/mcp/tools/transcribe/registerTranscribeAudioTool.ts
  • lib/prompts/getSystemPrompt.ts
  • lib/spotify/getSpotifyFollowers.ts
  • lib/supabase/account_artist_ids/getAccountArtistIds.ts
  • lib/supabase/account_workspace_ids/getAccountWorkspaceIds.ts
  • lib/supabase/files/createFileRecord.ts
  • lib/supabase/song_artists/insertSongArtists.ts
  • lib/supabase/storage/uploadFileByKey.ts
  • lib/transcribe/processAudioTranscription.ts
  • lib/transcribe/saveAudioToFiles.ts
  • lib/transcribe/saveTranscriptToFiles.ts
  • lib/transcribe/types.ts
  • lib/trigger/triggerCreateContent.ts

Comment on lines +13 to 14
* @param params.params
* @param params - Route params containing the platform name
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Incorrect JSDoc parameter: @param params.params is nonsensical.

The parameter structure is { params }: { params: Promise<{ platform: string }> }, so the correct documentation should reference params.platform, not params.params.

📝 Suggested fix
- * `@param` params.params
  * `@param` params - Route params containing the platform name
+ * `@param` params.platform - The platform identifier (e.g., "slack", "whatsapp")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* @param params.params
* @param params - Route params containing the platform name
* `@param` params - Route params containing the platform name
* `@param` params.platform - The platform identifier (e.g., "slack", "whatsapp")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/coding-agent/`[platform]/route.ts around lines 13 - 14, The JSDoc
incorrectly documents `@param params.params`; update the comment to reference
the actual parameter structure and property — replace `@param params.params`
with `@param params.platform` and update the description to indicate that
`params` is an object (or promise-resolved object) containing the `platform`
string used by the route handler (the handler signature is `{ params }: {
params: Promise<{ platform: string }> }`). Ensure the JSDoc matches the
handler's parameter name and property (`params.platform`) and remove the
nonsensical `params.params` entry.

Comment on lines +38 to 39
* @param params.params
* @param params - Route params containing the platform name
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same JSDoc error repeated for POST handler.

📝 Suggested fix
- * `@param` params.params
  * `@param` params - Route params containing the platform name
+ * `@param` params.platform - The platform identifier (e.g., "slack", "whatsapp")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/coding-agent/`[platform]/route.ts around lines 38 - 39, The JSDoc for
both the GET and POST route handlers contains a duplicate/incorrect `@param` entry
("@param params.params" and "@param params"); update the JSDoc for the GET and
POST functions (the exported GET and POST handlers) to remove the redundant
"@param params.params" line and keep a single correct parameter doc like "@param
params - Route params containing the platform name" (or rename to "@param
param0" if matching the handler signature), ensuring the doc matches the handler
signature exactly.

* Returns the most recent latest_verified_at (in ms) across all linked_accounts for a Privy user.
* Returns null if no linked account has a latest_verified_at.
*
* @param user
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use account-specific terminology in JSDoc param naming.

@param user conflicts with the repository terminology rule. Rename to account-focused wording (for example, @param accountHolder), and add a brief description for clarity.

As per coding guidelines, "Use 'account' terminology, never 'entity' or 'user'; use specific names like 'artist', 'workspace', 'organization' when referring to specific types".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/admins/privy/getLatestVerifiedAt.ts` at line 8, The JSDoc currently uses
"@param user" which violates the repo's account-first terminology; update the
JSDoc for getLatestVerifiedAt to use an account-focused name (for example
"@param accountHolder") and add a short description (e.g., "the account to
retrieve verification info for"). Also consider renaming the function parameter
from "user" to "accountHolder" in the getLatestVerifiedAt signature (and update
all internal references) so the doc and code consistently use account
terminology.

Comment on lines +39 to +50
if (tokenResult.error || !tokenResult.access_token) {
console.error("Failed to get Spotify token:", tokenResult.error);
return null;
}

const accessToken = tokenResult.access_token;

const searchResult = await getSearch({ q: artistName, type: "artist", limit: 1, accessToken });
if (searchResult.error || !searchResult.data) {
console.error("Spotify artist search failed:", searchResult.error);
return null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Differentiate “not found” from upstream Spotify failures.

This currently returns null for token/search errors and true not-found cases, so downstream code cannot return accurate status/error semantics.

🛠️ Suggested direction
+export type ArtistSpotifyDataResult =
+  | { type: "success"; data: ArtistSpotifyData }
+  | { type: "not_found" }
+  | { type: "error"; error: string };

-export async function getArtistSpotifyData(artistName: string): Promise<ArtistSpotifyData | null> {
+export async function getArtistSpotifyData(artistName: string): Promise<ArtistSpotifyDataResult> {
   const tokenResult = await generateAccessToken();
   if (tokenResult.error || !tokenResult.access_token) {
-    console.error("Failed to get Spotify token:", tokenResult.error);
-    return null;
+    return { type: "error", error: "Failed to authenticate with Spotify" };
   }
   ...
   if (!artist) {
-    return null;
+    return { type: "not_found" };
   }
   ...
-  return { artist, topTracks, previewUrl };
+  return { type: "success", data: { artist, topTracks, previewUrl } };
}

Then map not_found to 404 and error to 5xx in generateArtistIntelPack.

Also applies to: 59-62

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/getArtistSpotifyData.ts` around lines 39 - 50,
getArtistSpotifyData currently returns null for both upstream failures and true
"artist not found" cases, losing semantic difference; modify
getArtistSpotifyData to return a structured result (e.g., { status: "ok"|
"not_found" | "error", data?: Artist, error?: Error }) instead of null so
callers can distinguish outcomes when handling tokenResult and getSearch
responses (inspect tokenResult.error and searchResult.data to set status
appropriately). Then update generateArtistIntelPack to handle the new statuses:
map "not_found" to a 404 response and map "error" (upstream/token/search
failures) to a 5xx response, using the returned error details for logging;
ensure functions referenced are getArtistSpotifyData, tokenResult, getSearch,
and generateArtistIntelPack.

Comment on lines +64 to +67
const tracksData = topTracksResult.data as { tracks: SpotifyTrack[] };
const topTracks = tracksData.tracks || [];
const previewUrl = topTracks.find(t => t.preview_url)?.preview_url ?? null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep analyzed audio source consistent with reported top_track.

previewUrl comes from the first track with a preview, but top_track elsewhere is topTracks[0]. That can produce analysis for one track while reporting another as top track.

🎯 Suggested fix
-const previewUrl = topTracks.find(t => t.preview_url)?.preview_url ?? null;
+const previewUrl = topTracks[0]?.preview_url ?? null;

If you want fallback analysis from non-top tracks, return analyzed_track_id alongside previewUrl and expose it in the pack.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const tracksData = topTracksResult.data as { tracks: SpotifyTrack[] };
const topTracks = tracksData.tracks || [];
const previewUrl = topTracks.find(t => t.preview_url)?.preview_url ?? null;
const tracksData = topTracksResult.data as { tracks: SpotifyTrack[] };
const topTracks = tracksData.tracks || [];
const previewUrl = topTracks[0]?.preview_url ?? null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/getArtistSpotifyData.ts` around lines 64 - 67, The current
code sets previewUrl from the first track that has a preview while the reported
top_track is topTracks[0], causing a mismatch; change the logic in
getArtistSpotifyData so previewUrl is taken from topTracks[0] (tracksData /
topTracks) to guarantee the analyzed audio matches top_track, and if you want to
allow fallback analysis from a different track, return an additional
analyzed_track_id alongside previewUrl (e.g., set analyzed_track_id when you
pick a non-top track) and expose that field in the pack so callers know which
track was actually analyzed.

Comment on lines +15 to +20
* @param usage.usage
* @param usage - The language model usage data
* @param model - The model ID used for the chat
* @param accountId - The account ID to deduct credits from (optional)
* @param usage.model
* @param usage.accountId
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove incorrect JSDoc parameter annotations.

Lines 15, 19, and 20 use incorrect dot-notation (@param usage.usage, @param usage.model, @param usage.accountId) that doesn't match the actual function signature. The function uses destructured parameters {usage, model, accountId}, which are already correctly documented on lines 16-18 as top-level parameters.

These duplicate annotations create confusion and conflict with the accurate documentation.

📝 Proposed fix: Remove the incorrect parameter annotations
 /**
  * Handles credit deduction after chat completion.
  * Calculates usage cost and deducts appropriate credits from the user's account.
  *
- * `@param` usage.usage
  * `@param` usage - The language model usage data
  * `@param` model - The model ID used for the chat
  * `@param` accountId - The account ID to deduct credits from (optional)
- * `@param` usage.model
- * `@param` usage.accountId
  */
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* @param usage.usage
* @param usage - The language model usage data
* @param model - The model ID used for the chat
* @param accountId - The account ID to deduct credits from (optional)
* @param usage.model
* @param usage.accountId
/**
* Handles credit deduction after chat completion.
* Calculates usage cost and deducts appropriate credits from the user's account.
*
* `@param` usage - The language model usage data
* `@param` model - The model ID used for the chat
* `@param` accountId - The account ID to deduct credits from (optional)
*/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/credits/handleChatCredits.ts` around lines 15 - 20, The JSDoc contains
incorrect dotted param annotations that duplicate and conflict with the real
signature of the function handleChatCredits (which takes destructured params
{usage, model, accountId}); remove the erroneous lines "@param usage.usage",
"@param usage.model", and "@param usage.accountId" from the JSDoc so only the
correct top-level "@param usage", "@param model", and "@param accountId" entries
remain, leaving the rest of the comment intact.

Comment on lines +14 to +22
* @param regularEntries.regularEntries
* @param regularEntries - Non-submodule file tree entries
* @param submoduleEntries - Submodule references (type "commit" from GitHub Trees API)
* @param repo - Repository context for fetching .gitmodules
* @param regularEntries.submoduleEntries
* @param regularEntries.repo
* @param regularEntries.repo.owner
* @param regularEntries.repo.repo
* @param regularEntries.repo.branch
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

What is the recommended JSDoc style for destructured object parameters in TypeScript when documenting top-level keys versus nested keys?

💡 Result:

The recommended JSDoc style for destructured object parameters in TypeScript, for both top-level and nested keys, uses the @param tag with dot notation to document the object parameter and its properties. This approach is explicitly supported by official JSDoc documentation and works well with TypeScript's JSDoc integration. For top-level keys in a destructured parameter like function foo({a, b}) {}, document as: / * @param {Object} obj - The object parameter. * @param {string} obj.a - Description of a. * @param {number} [obj.b] - Optional description of b. */ function foo({a, b}) {} For nested keys, such as function foo({a, b: {c}}) {} or deeper nesting like {x: {y: {z}}}, extend the dot notation: / * Nesting example. * @param {Object} param - The parameter object. * @param {number} param.a - Top-level property. * @param {Object} param.b - Nested wrapper object. * @param {string} param.b.c - Nested property. */ const fn = ({a, b: {c}}) => a + c; This syntax documents the full shape of the incoming object, providing IntelliSense and hover information in TypeScript-aware editors like VS Code. TypeScript supports this JSDoc pattern, as confirmed in its JSDoc reference and wiki. Alternatives like inline object types (@param {{a: string, b: {c: number}}} obj) or @typedef for complex shapes are also common and recommended for reusability, especially for deeply nested or recursive structures: / * @typedef {Object} NestedConfig * @property {string} topLevel * @property {Object} nested * @property {number} nested.deep */ / * @param {NestedConfig} config */ function bar({topLevel, nested: {deep}}) {} This style is consistent across JSDoc 3.6+, TypeScript 5.x+, and style guides like Google's TypeScript Style Guide, which permits simple destructured parameters while favoring explicit typing for clarity.

Citations:


🏁 Script executed:

cat -n lib/github/expandSubmoduleEntries.ts | head -40

Repository: recoupable/api

Length of output: 1744


JSDoc describes the wrong parameter structure.

The tags on lines 14 and 18-22 describe fields under regularEntries.*, but the function actually takes a destructured object with three sibling top-level parameters: regularEntries, submoduleEntries, and repo. The incorrect nesting in JSDoc will mislead developers using IDE hints.

Remove the nested references (lines 14, 18-22) and document the top-level structure instead:

Suggested fix
- * `@param` regularEntries.regularEntries
  * `@param` regularEntries - Non-submodule file tree entries
  * `@param` submoduleEntries - Submodule references (type "commit" from GitHub Trees API)
  * `@param` repo - Repository context for fetching .gitmodules
- * `@param` regularEntries.submoduleEntries
- * `@param` regularEntries.repo
- * `@param` regularEntries.repo.owner
- * `@param` regularEntries.repo.repo
- * `@param` regularEntries.repo.branch
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* @param regularEntries.regularEntries
* @param regularEntries - Non-submodule file tree entries
* @param submoduleEntries - Submodule references (type "commit" from GitHub Trees API)
* @param repo - Repository context for fetching .gitmodules
* @param regularEntries.submoduleEntries
* @param regularEntries.repo
* @param regularEntries.repo.owner
* @param regularEntries.repo.repo
* @param regularEntries.repo.branch
* `@param` regularEntries - Non-submodule file tree entries
* `@param` submoduleEntries - Submodule references (type "commit" from GitHub Trees API)
* `@param` repo - Repository context for fetching .gitmodules
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/github/expandSubmoduleEntries.ts` around lines 14 - 22, The JSDoc
currently nests fields under regularEntries but the function
expandSubmoduleEntries actually accepts three top-level destructured parameters
(regularEntries, submoduleEntries, repo); update the comments to remove the
erroneous nested tags and instead document the three top-level parameters:
`@param` regularEntries - Non-submodule file tree entries, `@param` submoduleEntries
- Submodule references (type "commit" from GitHub Trees API), and `@param` repo -
Repository context (with repo.owner, repo.repo, repo.branch). Keep references to
repo.owner, repo.repo, and repo.branch as properties of the repo param and
remove any `@param` lines that prefix those with regularEntries.

Comment on lines +7 to +12
* @param owner.owner
* @param owner - The GitHub repository owner
* @param repo - The GitHub repository name
* @param branch - The branch to fetch from
* @param owner.repo
* @param owner.branch
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In JSDoc for a TypeScript function with signature function fn({ owner, repo, branch }: {...}), should @paramtags beowner, repo, branchrather thanowner.owner, owner.repo, owner.branch?

💡 Result:

No, @param tags should use owner, repo, branch (the destructured property names), not owner.owner, owner.repo, owner.branch. The standard JSDoc approach for a destructured parameter like function fn({ owner, repo, branch }) is to assign a name to the input object (e.g., options) and document its properties directly with that name as the prefix: / * @param {Object} options - The repository options. * @param {string} options.owner - Repository owner. * @param {string} options.repo - Repository name. * @param {string} options.branch - Branch name. */ function fn({ owner, repo, branch }) { ... } This is explicitly shown in the official JSDoc documentation for documenting destructuring parameters. TypeScript supports this JSDoc syntax, providing IntelliSense and type checking based on these tags. Using the destructured names alone (e.g., @param {string} owner) without the object prefix is not standard and may not reliably associate the documentation with the parameter structure.

Citations:


🏁 Script executed:

cat -n lib/github/getRepoGitModules.ts | head -30

Repository: recoupable/api

Length of output: 1190


Remove mismatched JSDoc param declarations.

Lines 7, 11, and 12 document nested properties (owner.owner, owner.repo, owner.branch), but the function signature destructures parameters directly (owner, repo, branch). This creates incorrect documentation that doesn't reflect the actual parameter structure.

Fix
- * `@param` owner.owner
  * `@param` owner - The GitHub repository owner
  * `@param` repo - The GitHub repository name
  * `@param` branch - The branch to fetch from
- * `@param` owner.repo
- * `@param` owner.branch
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* @param owner.owner
* @param owner - The GitHub repository owner
* @param repo - The GitHub repository name
* @param branch - The branch to fetch from
* @param owner.repo
* @param owner.branch
* `@param` owner - The GitHub repository owner
* `@param` repo - The GitHub repository name
* `@param` branch - The branch to fetch from
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/github/getRepoGitModules.ts` around lines 7 - 12, Update the JSDoc in
getRepoGitModules (lib/github/getRepoGitModules.ts) to remove incorrect nested
param names and match the function signature: replace any occurrences of
owner.owner, owner.repo, owner.branch with flat `@param` entries for owner, repo,
and branch (and remove duplicated/incorrect lines), ensuring the descriptions
reflect the actual parameters used by the function.

Comment on lines +9 to 13
* @param githubRepo.githubRepo
* @param githubRepo - The parent GitHub repository URL
* @param path - The file path to resolve
* @param githubRepo.path
* @returns The resolved repo URL and path
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

For TypeScript JSDoc on a function that destructures { githubRepo, path }directly in parameters, are@param githubRepoand@param paththe correct tags, and is@param githubRepo.path invalid in this case?

💡 Result:

No, @param githubRepo and @param path are not the correct tags for a function that destructures { githubRepo, path } directly in the parameters. The correct approach uses @param tags with dot notation on a named object parameter, such as @param {string} options - The options object. followed by @param {string} options.githubRepo - The GitHub repository. and @param {string} options.path - The file path.. Using plain @param githubRepo and @param path documents them as if they were separate top-level parameters, which mismatches the destructuring signature and can lead to TypeScript errors like "no parameter with that name". Additionally, @param githubRepo.path is invalid here because there is no top-level parameter named githubRepo with a path property—the destructuring assigns githubRepo and path as separate local variables from the single input object. The dot notation applies to properties of the documented object parameter, not the destructured variables. This is confirmed by the official JSDoc documentation on parameters with properties and destructuring, which explicitly shows the object-named approach for destructured params. TypeScript supports this JSDoc syntax for type checking and IntelliSense in JS files.

Citations:


🏁 Script executed:

fd "resolveSubmodulePath.ts" -t f --exec cat -n {}

Repository: recoupable/api

Length of output: 1808


JSDoc for destructured parameters requires a named parameter object with dot notation.

The current JSDoc is malformed, but the suggested fix is also incorrect. For a function with an unnamed destructured parameter { githubRepo, path }, JSDoc best practices require either:

  1. Rename the parameter to have an explicit name (e.g., options or params):

    export async function resolveSubmodulePath({
      githubRepo,
      path,
    }: {
      githubRepo: string;
      path: string;
    })

    Then document with:

    `@param` options - The submodule resolution options
    `@param` options.githubRepo - The parent GitHub repository URL
    `@param` options.path - The file path to resolve
    
  2. Or use destructuring JSDoc syntax that explicitly documents the unnamed object structure. Using @param githubRepo and @param path as standalone parameters misrepresents the actual signature and will confuse TypeScript tooling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/github/resolveSubmodulePath.ts` around lines 9 - 13, The JSDoc for
resolveSubmodulePath incorrectly documents destructured parameters; update the
function signature to use a named options object (e.g., rename the unnamed
destructured param to options in resolveSubmodulePath) and then change JSDoc to
`@param` options - The submodule resolution options, `@param` options.githubRepo -
The parent GitHub repository URL, and `@param` options.path - The file path to
resolve; alternatively, if you prefer to keep the inline destructuring, replace
the current `@param` lines with a single `@param` that documents the object shape
using JSDoc object/destructuring syntax so the docs match the actual parameter
structure used by resolveSubmodulePath.

* @param params.artistInstruction - The artist instruction
* @param params.conversationName - The name of the conversation
* @param params.accountWithDetails - The account with details
* @param params.orgId
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Complete the JSDoc parameter description.

The JSDoc for params.orgId is missing a description. All other parameters include descriptions (e.g., "The ID of the room", "The ID of the artist"), but this one is incomplete.

📝 Suggested fix
- * `@param` params.orgId
+ * `@param` params.orgId - The ID of the organization (null for personal accounts)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/prompts/getSystemPrompt.ts` at line 16, Add a clear description for the
JSDoc parameter params.orgId in the getSystemPrompt function's comment block:
replace the incomplete "@param params.orgId" with a descriptive line such as
"@param params.orgId - The ID of the organization" (matching wording/style used
for other params like roomId/artistId) so the JSDoc documents what orgId
represents.

…r web context, markdown report

- Add proper TypeScript types for MusicFlamingo analysis responses
  (CatalogMetadata, AudienceProfile, MoodTagsResult) replacing `unknown`
- Switch getArtistWebContext from Perplexity search snippets to
  chatWithPerplexity (sonar-pro) for richer narrative summaries with citations
- Add formatArtistIntelPackAsMarkdown: formats the full pack as a
  structured markdown report with sections for artist profile, music DNA,
  web context, and marketing pack
- Add formatted_report field to ArtistIntelPack returned by the API
- Update generate_artist_intel_pack MCP tool to return formatted markdown
  directly (instead of raw JSON) so the chat UI renders it beautifully
- 15 new formatter tests; 1529 total passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
lib/artistIntel/getArtistWebContext.ts (1)

20-27: ⚠️ Potential issue | 🟠 Major

Replace fixed “2024-2025” range with dynamic years.

At Line 22, the prompt hard-codes 2024-2025. On March 21, 2026, this biases results away from 2026 coverage and can degrade “current/recent” context quality.

Suggested fix
 export async function getArtistWebContext(artistName: string): Promise<ArtistWebContext | null> {
   try {
+    const currentYear = new Date().getUTCFullYear();
+    const previousYear = currentYear - 1;
     const result = await chatWithPerplexity([
       {
         role: "user",
         content: `Research the music artist "${artistName}". Provide a concise but rich overview covering:
 1. Who they are and their current career stage
-2. Recent releases, streaming milestones, or news (2024-2025)
+2. Recent releases, streaming milestones, or news (${previousYear}-${currentYear})
 3. Their core sound and genre positioning
 4. Notable collaborations, press coverage, or cultural moments
 5. Why they matter right now in the music industry
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/getArtistWebContext.ts` around lines 20 - 27, The prompt in
getArtistWebContext.ts hard-codes the year range "2024-2025" in the content
template, which will become stale; update the template in the content field used
by getArtistWebContext to compute the current year dynamically (e.g., const
currentYear = new Date().getFullYear() and previousYear = currentYear - 1) and
inject those values into the prompt string instead of the fixed "2024-2025" text
so the “current/recent” range always reflects the present year.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/artistIntel/formatArtistIntelPackAsMarkdown.ts`:
- Around line 13-157: The formatter function formatArtistIntelPackAsMarkdown is
doing too many things; split it into smaller section-builder functions (e.g.,
buildArtistProfileSection(pack.artist, pack.top_track),
buildMusicDNASection(pack.music_analysis),
buildWebContextSection(pack.web_context),
buildMarketingPackSection(pack.marketing_pack)) that each return an array/string
of markdown lines, then have formatArtistIntelPackAsMarkdown simply compose
those pieces and join them; refactor logic for BPM/Key/Mood, audience, moods,
playlist_pitch, citations, and key_talking_points into their respective builders
and keep each builder under ~50 lines and single-responsibility so the top-level
function only orchestrates composition.

In `@lib/artistIntel/getArtistMusicAnalysis.ts`:
- Around line 90-99: The code in getArtistMusicAnalysis is assuming result.value
has a response when result.status === "fulfilled" and result.value.type ===
"success"; instead, first narrow the union by checking the actual payload shape
(e.g., verify "response" in result.value or that the object matches
AnalysisSuccess rather than FullReportSuccess) before casting and reading
value.response; update the branch that assigns analysis.catalog_metadata /
analysis.audience_profile / analysis.playlist_pitch / analysis.mood_tags to only
read value.response when the payload contains response, else handle or ignore
FullReportSuccess (which has report) appropriately so you don't assign
undefined.

---

Duplicate comments:
In `@lib/artistIntel/getArtistWebContext.ts`:
- Around line 20-27: The prompt in getArtistWebContext.ts hard-codes the year
range "2024-2025" in the content template, which will become stale; update the
template in the content field used by getArtistWebContext to compute the current
year dynamically (e.g., const currentYear = new Date().getFullYear() and
previousYear = currentYear - 1) and inject those values into the prompt string
instead of the fixed "2024-2025" text so the “current/recent” range always
reflects the present year.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 183f51fb-4b8e-4fec-87f3-1a6205318558

📥 Commits

Reviewing files that changed from the base of the PR and between 57cf714 and ee55513.

⛔ Files ignored due to path filters (2)
  • lib/artistIntel/__tests__/formatArtistIntelPackAsMarkdown.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/artistIntel/__tests__/generateArtistIntelPack.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (5)
  • lib/artistIntel/formatArtistIntelPackAsMarkdown.ts
  • lib/artistIntel/generateArtistIntelPack.ts
  • lib/artistIntel/getArtistMusicAnalysis.ts
  • lib/artistIntel/getArtistWebContext.ts
  • lib/mcp/tools/artistIntel/registerGenerateArtistIntelPackTool.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/mcp/tools/artistIntel/registerGenerateArtistIntelPackTool.ts
  • lib/artistIntel/generateArtistIntelPack.ts

Comment on lines +13 to +157
export function formatArtistIntelPackAsMarkdown(pack: ArtistIntelPack): string {
const { artist, top_track, music_analysis, web_context, marketing_pack, elapsed_seconds } = pack;

const lines: string[] = [];

lines.push(`# Artist Intelligence Pack: ${artist.name}`);
lines.push(``);
lines.push(`> Generated in ${elapsed_seconds}s · Spotify + MusicFlamingo AI + Perplexity`);
lines.push(``);
lines.push(`---`);
lines.push(``);

// Artist profile
lines.push(`## Artist Profile`);
lines.push(``);
lines.push(`- **Followers:** ${artist.followers.toLocaleString()}`);
lines.push(`- **Popularity:** ${artist.popularity}/100`);
if (artist.genres.length > 0) {
lines.push(`- **Genres:** ${artist.genres.slice(0, 4).join(", ")}`);
}
if (top_track) {
lines.push(
`- **Top Track:** "${top_track.name}" — *${top_track.album_name}* (popularity: ${top_track.popularity}/100)`,
);
if (top_track.preview_url) {
lines.push(`- **Preview:** ${top_track.preview_url}`);
}
}
lines.push(``);
lines.push(`---`);
lines.push(``);

// Music DNA
if (music_analysis) {
lines.push(`## Music DNA (NVIDIA MusicFlamingo AI)`);
lines.push(``);

const meta = music_analysis.catalog_metadata;
if (meta) {
const genreLine = meta.subgenres?.length
? `**Genre:** ${meta.genre} *(${meta.subgenres.join(", ")})*`
: `**Genre:** ${meta.genre}`;
lines.push(genreLine);
lines.push(
`**BPM:** ${meta.tempo_bpm} | **Key:** ${meta.key} | **Time:** ${meta.time_signature}`,
);
lines.push(`**Energy:** ${meta.energy_level}/10 | **Danceability:** ${meta.danceability}/10`);
if (meta.mood?.length) lines.push(`**Mood:** ${meta.mood.join(", ")}`);
if (meta.instruments?.length) lines.push(`**Instruments:** ${meta.instruments.join(", ")}`);
if (meta.vocal_style) lines.push(`**Vocal Style:** ${meta.vocal_style}`);
if (meta.production_style) lines.push(`**Production:** ${meta.production_style}`);
if (meta.similar_artists?.length)
lines.push(`**Sounds Like:** ${meta.similar_artists.join(", ")}`);
if (meta.description) lines.push(`**Description:** ${meta.description}`);
lines.push(``);
}

const audience = music_analysis.audience_profile;
if (audience) {
lines.push(
`**Target Audience:** ${audience.age_range}${audience.gender_skew !== "neutral" ? `, ${audience.gender_skew}` : ""}`,
);
if (audience.listening_contexts?.length) {
lines.push(
`**Listening Contexts:** ${audience.listening_contexts.slice(0, 3).join(" · ")}`,
);
}
if (audience.platforms?.length) {
lines.push(`**Key Platforms:** ${audience.platforms.join(", ")}`);
}
if (audience.comparable_fanbases?.length) {
lines.push(`**Comparable Fanbases:** ${audience.comparable_fanbases.join(", ")}`);
}
if (audience.marketing_hook) {
lines.push(`**Marketing Hook:** *"${audience.marketing_hook}"*`);
}
lines.push(``);
}

const moods = music_analysis.mood_tags;
if (moods?.tags?.length) {
lines.push(`**Vibe Tags:** ${moods.tags.join(" · ")}`);
if (moods.primary_mood) lines.push(`**Primary Mood:** ${moods.primary_mood}`);
lines.push(``);
}

if (music_analysis.playlist_pitch) {
lines.push(`**Playlist Pitch (AI-generated from audio):**`);
lines.push(``);
lines.push(music_analysis.playlist_pitch);
lines.push(``);
}

lines.push(`---`);
lines.push(``);
}

// Web context
if (web_context?.summary) {
lines.push(`## Recent Web Context`);
lines.push(``);
lines.push(web_context.summary);
lines.push(``);
if (web_context.citations?.length) {
lines.push(`*Sources: ${web_context.citations.slice(0, 3).join(", ")}*`);
lines.push(``);
}
lines.push(`---`);
lines.push(``);
}

// Marketing pack
lines.push(`## Marketing Pack`);
lines.push(``);

lines.push(`### Playlist Pitch Email`);
lines.push(``);
lines.push(marketing_pack.playlist_pitch_email);
lines.push(``);

lines.push(`### Social Media Captions`);
lines.push(``);
lines.push(`**Instagram:** ${marketing_pack.instagram_caption}`);
lines.push(``);
lines.push(`**TikTok:** ${marketing_pack.tiktok_caption}`);
lines.push(``);
lines.push(`**Twitter/X:** ${marketing_pack.twitter_post}`);
lines.push(``);

lines.push(`### Press Release Opener`);
lines.push(``);
lines.push(marketing_pack.press_release_opener);
lines.push(``);

if (marketing_pack.key_talking_points?.length) {
lines.push(`### Key Talking Points`);
lines.push(``);
for (const point of marketing_pack.key_talking_points) {
lines.push(`- ${point}`);
}
lines.push(``);
}

return lines.join("\n").trim();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Split this formatter into section builders (current function is too large/multi-purpose).

At Line 13-Line 157, this function handles multiple distinct concerns in one body and is well over the project’s size target for domain functions, which will make future edits risky.

Refactor direction
 export function formatArtistIntelPackAsMarkdown(pack: ArtistIntelPack): string {
-  const { artist, top_track, music_analysis, web_context, marketing_pack, elapsed_seconds } = pack;
-  const lines: string[] = [];
-  ...
-  return lines.join("\n").trim();
+  const lines: string[] = [];
+  lines.push(...buildHeaderSection(pack));
+  lines.push(...buildArtistProfileSection(pack));
+  lines.push(...buildMusicDnaSection(pack.music_analysis));
+  lines.push(...buildWebContextSection(pack.web_context));
+  lines.push(...buildMarketingPackSection(pack.marketing_pack));
+  return lines.join("\n").trim();
 }

As per coding guidelines, lib/**/*.ts: “Single responsibility per function” and “Keep functions under 50 lines.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/formatArtistIntelPackAsMarkdown.ts` around lines 13 - 157,
The formatter function formatArtistIntelPackAsMarkdown is doing too many things;
split it into smaller section-builder functions (e.g.,
buildArtistProfileSection(pack.artist, pack.top_track),
buildMusicDNASection(pack.music_analysis),
buildWebContextSection(pack.web_context),
buildMarketingPackSection(pack.marketing_pack)) that each return an array/string
of markdown lines, then have formatArtistIntelPackAsMarkdown simply compose
those pieces and join them; refactor logic for BPM/Key/Mood, audience, moods,
playlist_pitch, citations, and key_talking_points into their respective builders
and keep each builder under ~50 lines and single-responsibility so the top-level
function only orchestrates composition.

Comment on lines +90 to +99
if (result.status === "fulfilled" && result.value.type === "success") {
const value = result.value as { type: "success"; response: unknown };
if (preset === "catalog_metadata") {
analysis.catalog_metadata = value.response as CatalogMetadata;
} else if (preset === "audience_profile") {
analysis.audience_profile = value.response as AudienceProfile;
} else if (preset === "playlist_pitch") {
analysis.playlist_pitch = value.response as string;
} else if (preset === "mood_tags") {
analysis.mood_tags = value.response as MoodTagsResult;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== AnalyzeMusicResult variants =="
fd processAnalyzeMusicRequest.ts --exec sed -n '1,140p' {}

echo
echo "== Current narrowing in getArtistMusicAnalysis =="
fd getArtistMusicAnalysis.ts --exec sed -n '80,120p' {}

Repository: recoupable/api

Length of output: 4331


Guard success payload shape before reading response.

At lines 90–99, checking type === "success" alone cannot distinguish between FullReportSuccess (which has a report field) and AnalysisSuccess (which has a response field). The type assertion bypasses this gap, allowing the code to access value.response even when the object is actually a FullReportSuccess. This silently assigns undefined instead of proper analysis data.

Suggested fix
   results.forEach((result, i) => {
     const preset = ANALYSIS_PRESETS[i] as AnalysisPreset;
-    if (result.status === "fulfilled" && result.value.type === "success") {
-      const value = result.value as { type: "success"; response: unknown };
+    if (
+      result.status === "fulfilled" &&
+      result.value.type === "success" &&
+      "response" in result.value
+    ) {
+      const value = result.value;
       if (preset === "catalog_metadata") {
         analysis.catalog_metadata = value.response as CatalogMetadata;
       } else if (preset === "audience_profile") {
         analysis.audience_profile = value.response as AudienceProfile;
       } else if (preset === "playlist_pitch") {
         analysis.playlist_pitch = value.response as string;
       } else if (preset === "mood_tags") {
         analysis.mood_tags = value.response as MoodTagsResult;
       }
       anySuccess = true;
     }
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/getArtistMusicAnalysis.ts` around lines 90 - 99, The code in
getArtistMusicAnalysis is assuming result.value has a response when
result.status === "fulfilled" and result.value.type === "success"; instead,
first narrow the union by checking the actual payload shape (e.g., verify
"response" in result.value or that the object matches AnalysisSuccess rather
than FullReportSuccess) before casting and reading value.response; update the
branch that assigns analysis.catalog_metadata / analysis.audience_profile /
analysis.playlist_pitch / analysis.mood_tags to only read value.response when
the payload contains response, else handle or ignore FullReportSuccess (which
has report) appropriately so you don't assign undefined.

…ts & labels

Replace generic social media captions as the primary output with high-value
music industry materials: artist one-sheet, A&R memo, sync licensing brief,
named Spotify editorial playlist targets, and brand partnership pitch.

Social captions remain but are moved to a secondary 'Outreach & Social' section.
The Industry Pack section now leads with the outputs that drive actual revenue
and career decisions for artists, managers, and label A&R teams.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
lib/artistIntel/buildArtistMarketingCopy.ts (2)

37-114: Function exceeds 50-line guideline; consider extracting helpers.

The function spans ~78 lines, violating the coding guideline to "Keep functions under 50 lines." The responsibilities—prompt construction, AI invocation, response parsing, and fallback generation—could be split for improved testability and adherence to Single Responsibility Principle.

As per coding guidelines: "Keep functions under 50 lines."

♻️ Suggested decomposition
// Helper: build the AI prompt
function buildMarketingPrompt(
  artist: SpotifyArtist,
  topTrack: SpotifyTrack | null,
  musicAnalysis: ArtistMusicAnalysis | null,
  webContext: ArtistWebContext | null,
): string {
  // ... prompt template logic (~40 lines)
}

// Helper: generate fallback copy
function buildFallbackMarketingCopy(
  artist: SpotifyArtist,
  topTrack: SpotifyTrack | null,
): ArtistMarketingCopy {
  // ... fallback object construction (~25 lines)
}

// Main function stays lean
export async function buildArtistMarketingCopy(...): Promise<ArtistMarketingCopy> {
  const { artist, topTracks } = spotifyData;
  const topTrack = topTracks[0] ?? null;
  const prompt = buildMarketingPrompt(artist, topTrack, musicAnalysis, webContext);
  
  try {
    const result = await generateText({ system: SYSTEM_PROMPT, prompt });
    // ... parse and return
  } catch (error) {
    console.error("Failed to generate marketing copy:", error);
    return buildFallbackMarketingCopy(artist, topTrack);
  }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/buildArtistMarketingCopy.ts` around lines 37 - 114, The
function buildArtistMarketingCopy is too long and mixes responsibilities (prompt
construction, AI call, parsing, fallback); extract a helper
buildMarketingPrompt(artist, topTrack, musicAnalysis, webContext) to return the
long prompt string and another helper buildFallbackMarketingCopy(artist,
topTrack) to construct the fallback ArtistMarketingCopy object, then simplify
buildArtistMarketingCopy to assemble topTrack, call buildMarketingPrompt, await
generateText({ system: SYSTEM_PROMPT, prompt }), JSON.parse the result, and on
catch call buildFallbackMarketingCopy while preserving the existing logging
behavior; keep signature and return types the same.

42-43: Consider explicit guard for empty topTracks array.

Per getArtistSpotifyData.ts (line 61), topTracks can be an empty array when the track fetch fails. While the downstream optional chaining (topTrack?.name) prevents runtime errors, an explicit guard improves readability and intent.

♻️ Suggested improvement
 const { artist, topTracks } = spotifyData;
-const topTrack = topTracks[0];
+const topTrack = topTracks.length > 0 ? topTracks[0] : null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/buildArtistMarketingCopy.ts` around lines 42 - 43, Replace
the direct indexing of topTracks with an explicit guard: instead of const
topTrack = topTracks[0]; check that topTracks is an array and has length > 0
(e.g., if (!Array.isArray(topTracks) || topTracks.length === 0) { /* handle
missing topTrack: set topTrack = undefined or return/compose fallback marketing
copy */ }) and then safely assign topTrack when present; update
buildArtistMarketingCopy's logic where topTrack is used to rely on this clear
guard (symbols: topTracks, topTrack, buildArtistMarketingCopy).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/artistIntel/buildArtistMarketingCopy.ts`:
- Around line 87-90: In buildArtistMarketingCopy, validate that generateText
returned a non-empty result.text before calling JSON.parse (follow the pattern
in lib/evals/extractTextFromResult.ts): if result.text is falsy, throw or return
a clear error indicating "AI returned no text". After parsing, do not blindly
assert with "as ArtistMarketingCopy"; instead perform runtime validation of
required fields on the parsed object (e.g., check presence and types of title,
description, callToAction, etc.) and throw a descriptive error if validation
fails so malformed/missing fields are caught early. Ensure these checks are
applied around the generateText call and replace the unsafe JSON.parse+type
assertion in buildArtistMarketingCopy.

---

Nitpick comments:
In `@lib/artistIntel/buildArtistMarketingCopy.ts`:
- Around line 37-114: The function buildArtistMarketingCopy is too long and
mixes responsibilities (prompt construction, AI call, parsing, fallback);
extract a helper buildMarketingPrompt(artist, topTrack, musicAnalysis,
webContext) to return the long prompt string and another helper
buildFallbackMarketingCopy(artist, topTrack) to construct the fallback
ArtistMarketingCopy object, then simplify buildArtistMarketingCopy to assemble
topTrack, call buildMarketingPrompt, await generateText({ system: SYSTEM_PROMPT,
prompt }), JSON.parse the result, and on catch call buildFallbackMarketingCopy
while preserving the existing logging behavior; keep signature and return types
the same.
- Around line 42-43: Replace the direct indexing of topTracks with an explicit
guard: instead of const topTrack = topTracks[0]; check that topTracks is an
array and has length > 0 (e.g., if (!Array.isArray(topTracks) ||
topTracks.length === 0) { /* handle missing topTrack: set topTrack = undefined
or return/compose fallback marketing copy */ }) and then safely assign topTrack
when present; update buildArtistMarketingCopy's logic where topTrack is used to
rely on this clear guard (symbols: topTracks, topTrack,
buildArtistMarketingCopy).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 19d7143d-e649-4e82-b598-c3126e9593a1

📥 Commits

Reviewing files that changed from the base of the PR and between ee55513 and a65399a.

⛔ Files ignored due to path filters (2)
  • lib/artistIntel/__tests__/formatArtistIntelPackAsMarkdown.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/artistIntel/__tests__/generateArtistIntelPack.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (2)
  • lib/artistIntel/buildArtistMarketingCopy.ts
  • lib/artistIntel/formatArtistIntelPackAsMarkdown.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/artistIntel/formatArtistIntelPackAsMarkdown.ts

Comment on lines +87 to +90
try {
const result = await generateText({ system: SYSTEM_PROMPT, prompt });
const parsed = JSON.parse(result.text) as ArtistMarketingCopy;
return parsed;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate result.text before parsing to avoid misleading errors.

Per the pattern in lib/evals/extractTextFromResult.ts, result.text can be undefined or falsy. Calling JSON.parse(result.text) when result.text is undefined produces SyntaxError: "undefined" is not valid JSON, which obscures the real issue (AI returned no text).

Additionally, the as ArtistMarketingCopy type assertion is unsafe—the parsed JSON may be missing required fields or have incorrect types.

🛡️ Proposed defensive parsing
 try {
   const result = await generateText({ system: SYSTEM_PROMPT, prompt });
-  const parsed = JSON.parse(result.text) as ArtistMarketingCopy;
-  return parsed;
+  if (typeof result.text !== "string" || !result.text.trim()) {
+    throw new Error("AI returned empty or invalid response");
+  }
+  const parsed: unknown = JSON.parse(result.text);
+  // Consider adding runtime validation (e.g., Zod) for production robustness
+  return parsed as ArtistMarketingCopy;
 } catch (error) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const result = await generateText({ system: SYSTEM_PROMPT, prompt });
const parsed = JSON.parse(result.text) as ArtistMarketingCopy;
return parsed;
try {
const result = await generateText({ system: SYSTEM_PROMPT, prompt });
if (typeof result.text !== "string" || !result.text.trim()) {
throw new Error("AI returned empty or invalid response");
}
const parsed: unknown = JSON.parse(result.text);
// Consider adding runtime validation (e.g., Zod) for production robustness
return parsed as ArtistMarketingCopy;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/buildArtistMarketingCopy.ts` around lines 87 - 90, In
buildArtistMarketingCopy, validate that generateText returned a non-empty
result.text before calling JSON.parse (follow the pattern in
lib/evals/extractTextFromResult.ts): if result.text is falsy, throw or return a
clear error indicating "AI returned no text". After parsing, do not blindly
assert with "as ArtistMarketingCopy"; instead perform runtime validation of
required fields on the parsed object (e.g., check presence and types of title,
description, callToAction, etc.) and throw a descriptive error if validation
fails so malformed/missing fields are caught early. Ensure these checks are
applied around the generateText call and replace the unsafe JSON.parse+type
assertion in buildArtistMarketingCopy.

- Add peer benchmarking via Spotify related-artists API — actual follower counts
  and popularity scores for 5 related artists, with percentile rankings showing
  exactly where the target artist sits among their competitive set
- Add algorithmic opportunity scores (0-100) computed from real MusicFlamingo
  audio data and Spotify metrics — no AI inference, pure signal:
  Sync Score (BPM, energy, mood diversity, production), Playlist Score
  (danceability, energy, algorithmic momentum), A&R Score (popularity-to-follower
  efficiency, peer gap analysis), Brand Score (lifestyle tags, demographic specificity)
- Add catalog depth analysis across all 10 top tracks: consistency score,
  hit-concentration %, catalog type classification (consistent / hit-driven / emerging)
- Ground all AI marketing copy in real competitor data — the prompt now receives
  actual peer follower counts so 'comparable artist' references cite real numbers
  not hallucinations
- Upgrade markdown report with: Opportunity Scores dashboard table with ASCII bars,
  Peer Benchmarking table with gap-to-peers column and YOU marker, Catalog Analysis
  with per-track popularity bars and actionable catalog type callout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (1)
lib/artistIntel/computeArtistOpportunityScores.ts (1)

46-274: Split the four scoring models into private helpers.

computeArtistOpportunityScores() now owns four separate heuristics, rationale assembly, clamping, and weighting in one body, which makes threshold changes hard to test or reason about safely. Extract per-domain calculators and keep this exported function focused on composition. As per coding guidelines, lib/**/*.ts: “Single responsibility per function”, “Keep functions under 50 lines”, and “DRY: Consolidate similar logic into shared utilities”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/computeArtistOpportunityScores.ts` around lines 46 - 274,
computeArtistOpportunityScores is doing four distinct heuristics in one large
function; extract each domain into a private helper to improve testability and
single responsibility. Create four helpers (e.g., computeSyncScore,
computePlaylistScore, computeArScore, computeBrandScore) that accept the
relevant inputs (musicAnalysis/meta, followers, popularity, peerBenchmark) and
return a consistent shape like { score: number, rationale: string[] }; move
domain-specific clamping/rating logic into either each helper or a small shared
utility (e.g., clampTo100, buildRationale) so duplicate min/max/rationale joins
are removed; then refactor computeArtistOpportunityScores to call those helpers,
compute the weighted overall, and return the same public shape using rating(...)
and joined rationales. Ensure helper names match the ones above to make locating
code straightforward and keep each function under ~50 lines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/artistIntel/analyzeCatalogDepth.ts`:
- Around line 34-42: The avg_popularity value is being rounded before it's used
to compute variance/popularity_std_dev which alters the std-dev; compute a raw
mean (e.g., raw_avg or mean) from popularities using track_count and use that
raw value in the variance/popularity_std_dev calculation, then only round values
(avg_popularity and popularity_std_dev) when building the returned payload;
update references to avg_popularity in the variance calculation to use the new
raw mean and keep the rounded avg_popularity for output only.

In `@lib/artistIntel/formatArtistIntelPackAsMarkdown.ts`:
- Around line 95-124: The formatter currently assumes opportunity_scores exists
and crashes; update formatArtistIntelPackAsMarkdown to guard or normalize before
pushing lines: check if opportunity_scores is present on the pack (or create a
default opportunity_scores object with safe defaults for overall,
sync/playlist/ar/brand {score, rating, rationale}) and then use those safe
values when calling ratingEmoji(...) and scoreBar(...); alternatively wrap the
whole Opportunity Scores block in a presence check for opportunity_scores and
skip it if missing, ensuring all references (opportunity_scores.overall,
opportunity_scores.sync.score, opportunity_scores.sync.rating,
opportunity_scores.sync.rationale, and similarly for playlist/ar/brand) and
calls to ratingEmoji and scoreBar only run against validated/defaulted data.

In `@lib/artistIntel/generateArtistIntelPack.ts`:
- Around line 74-79: The current Promise.all call for optional enrichments
(getArtistMusicAnalysis, getArtistWebContext, getRelatedArtistsData) must be
made "fail soft": replace the Promise.all with Promise.allSettled (or wrap each
call in a try/catch) and map any rejected result to null so that failures from
MusicFlamingo, Perplexity, or related-artists do not throw the whole pack;
ensure spotifyData.previewUrl still gates getArtistMusicAnalysis and that the
resolved values are assigned back to the same variables (musicAnalysis,
webContext, peerBenchmark) with null fallbacks on error.

In `@lib/artistIntel/getRelatedArtistsData.ts`:
- Around line 1-2: The module currently imports generateAccessToken at top-level
which throws when Spotify creds are missing; change to lazy-load it inside
getRelatedArtistsData (or move the env validation into generateAccessToken) so
module evaluation never throws: update getRelatedArtistsData to require/import
or call generateAccessToken dynamically at runtime before using it (and keep
getRelatedArtists imported as needed), ensuring when creds are absent the
function returns null instead of causing an import-time exception.
- Around line 60-68: Percentile calculation in the percentile(arr:number[],
value:number) function ignores ties (uses v < value), which makes tied
popularities report 0; change the logic to count values strictly less
(countLess) and values equal (countEqual) and compute percentile as
round(((countLess + 0.5 * countEqual) / arr.length) * 100), and handle empty
arrays defensively (e.g., return 0); update the percentile function (and any
callers that expect popularity_percentile) to use this tied-aware formula while
leaving median(...) unchanged.
- Around line 44-48: The current code slices related before sorting which only
ranks the first 10 items; instead sort the entire related array by followers
(b.followers.total - a.followers.total) and then take the top 5, i.e. replace
the chain "related.slice(0, 10).sort(...).slice(0, 5)" with a
sort-first-then-slice approach so the peers variable truly contains the top 5
most-followed artists; update the peers assignment in getRelatedArtistsData (the
peers/related logic) accordingly.

In `@lib/spotify/getRelatedArtists.ts`:
- Around line 15-19: The Spotify fetch in getRelatedArtists currently has no
cancellation; wrap the request with an AbortController and a short timeout
(e.g., 2–5s) that calls controller.abort() via setTimeout, pass
controller.signal to the fetch call, and catch the abort error; in the
getRelatedArtists function treat an aborted/timeout error the same as other soft
failures (returning the existing fallback path), and ensure you clear the
timeout on success to avoid leaks.

---

Nitpick comments:
In `@lib/artistIntel/computeArtistOpportunityScores.ts`:
- Around line 46-274: computeArtistOpportunityScores is doing four distinct
heuristics in one large function; extract each domain into a private helper to
improve testability and single responsibility. Create four helpers (e.g.,
computeSyncScore, computePlaylistScore, computeArScore, computeBrandScore) that
accept the relevant inputs (musicAnalysis/meta, followers, popularity,
peerBenchmark) and return a consistent shape like { score: number, rationale:
string[] }; move domain-specific clamping/rating logic into either each helper
or a small shared utility (e.g., clampTo100, buildRationale) so duplicate
min/max/rationale joins are removed; then refactor
computeArtistOpportunityScores to call those helpers, compute the weighted
overall, and return the same public shape using rating(...) and joined
rationales. Ensure helper names match the ones above to make locating code
straightforward and keep each function under ~50 lines.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 968f891d-420e-4f95-8c2e-4aacef8d8b09

📥 Commits

Reviewing files that changed from the base of the PR and between a65399a and 52ee805.

📒 Files selected for processing (7)
  • lib/artistIntel/analyzeCatalogDepth.ts
  • lib/artistIntel/buildArtistMarketingCopy.ts
  • lib/artistIntel/computeArtistOpportunityScores.ts
  • lib/artistIntel/formatArtistIntelPackAsMarkdown.ts
  • lib/artistIntel/generateArtistIntelPack.ts
  • lib/artistIntel/getRelatedArtistsData.ts
  • lib/spotify/getRelatedArtists.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/artistIntel/buildArtistMarketingCopy.ts

Comment on lines +34 to +42
const popularities = topTracks.map(t => t.popularity);
const track_count = topTracks.length;
const avg_popularity = Math.round(popularities.reduce((s, p) => s + p, 0) / track_count);
const top_track_popularity = Math.max(...popularities);

// Standard deviation
const variance =
popularities.reduce((sum, p) => sum + Math.pow(p - avg_popularity, 2), 0) / track_count;
const popularity_std_dev = Math.round(Math.sqrt(variance) * 10) / 10;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use the unrounded mean for the variance math.

Line 36 rounds the mean before Lines 40-42 compute popularity_std_dev, which can nudge consistency_score and flip borderline catalog labels. Keep a raw average for the calculations and round only in the returned payload.

Possible fix
-  const avg_popularity = Math.round(popularities.reduce((s, p) => s + p, 0) / track_count);
+  const avgPopularityRaw = popularities.reduce((s, p) => s + p, 0) / track_count;
+  const avg_popularity = Math.round(avgPopularityRaw);
   const top_track_popularity = Math.max(...popularities);
 
   // Standard deviation
   const variance =
-    popularities.reduce((sum, p) => sum + Math.pow(p - avg_popularity, 2), 0) / track_count;
+    popularities.reduce((sum, p) => sum + Math.pow(p - avgPopularityRaw, 2), 0) / track_count;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/analyzeCatalogDepth.ts` around lines 34 - 42, The
avg_popularity value is being rounded before it's used to compute
variance/popularity_std_dev which alters the std-dev; compute a raw mean (e.g.,
raw_avg or mean) from popularities using track_count and use that raw value in
the variance/popularity_std_dev calculation, then only round values
(avg_popularity and popularity_std_dev) when building the returned payload;
update references to avg_popularity in the variance calculation to use the new
raw mean and keep the rounded avg_popularity for output only.

Comment on lines +95 to +124
lines.push(`## Opportunity Scores`);
lines.push(``);
lines.push(
`> Algorithmically computed from real audio data and Spotify metrics — not AI-generated text.`,
);
lines.push(``);
lines.push(`**Overall Score: ${opportunity_scores.overall}/100**`);
lines.push(``);
lines.push(`| Category | Score | Rating | Signal |`);
lines.push(`|----------|-------|--------|--------|`);
lines.push(
`| 🎬 Sync & Licensing | ${opportunity_scores.sync.score}/100 | ${ratingEmoji(opportunity_scores.sync.rating)} ${opportunity_scores.sync.rating} | ${scoreBar(opportunity_scores.sync.score)} |`,
);
lines.push(
`| 🎵 Playlist Placement | ${opportunity_scores.playlist.score}/100 | ${ratingEmoji(opportunity_scores.playlist.rating)} ${opportunity_scores.playlist.rating} | ${scoreBar(opportunity_scores.playlist.score)} |`,
);
lines.push(
`| 🎤 A&R Priority | ${opportunity_scores.ar.score}/100 | ${ratingEmoji(opportunity_scores.ar.rating)} ${opportunity_scores.ar.rating} | ${scoreBar(opportunity_scores.ar.score)} |`,
);
lines.push(
`| 🤝 Brand Partnership | ${opportunity_scores.brand.score}/100 | ${ratingEmoji(opportunity_scores.brand.rating)} ${opportunity_scores.brand.rating} | ${scoreBar(opportunity_scores.brand.score)} |`,
);
lines.push(``);

lines.push(`**What's driving these scores:**`);
lines.push(``);
lines.push(`- **Sync:** ${opportunity_scores.sync.rationale}`);
lines.push(`- **Playlist:** ${opportunity_scores.playlist.rationale}`);
lines.push(`- **A&R:** ${opportunity_scores.ar.rationale}`);
lines.push(`- **Brand:** ${opportunity_scores.brand.rationale}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t let the new opportunity dashboard hard-fail formatting.

The current pipeline is failing on Line 101 because some existing pack shapes still reach this formatter without opportunity_scores. Please either guard this block or normalize defaults before formatting so one missing enrichment does not prevent the rest of the report from rendering.

🧰 Tools
🪛 GitHub Actions: Unit Tests

[error] 101-101: TypeError: Cannot read properties of undefined (reading 'overall') at opportunity_scores.overall while building Markdown (affects 21 tests in formatArtistIntelPackAsMarkdown.test.ts).

🪛 GitHub Check: test

[failure] 101-101: lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts > formatArtistIntelPackAsMarkdown > includes industry pack sections
TypeError: Cannot read properties of undefined (reading 'overall')
❯ formatArtistIntelPackAsMarkdown lib/artistIntel/formatArtistIntelPackAsMarkdown.ts:101:53
❯ lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts:155:20


[failure] 101-101: lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts > formatArtistIntelPackAsMarkdown > includes web context and citations
TypeError: Cannot read properties of undefined (reading 'overall')
❯ formatArtistIntelPackAsMarkdown lib/artistIntel/formatArtistIntelPackAsMarkdown.ts:101:53
❯ lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts:148:20


[failure] 101-101: lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts > formatArtistIntelPackAsMarkdown > includes mood tags
TypeError: Cannot read properties of undefined (reading 'overall')
❯ formatArtistIntelPackAsMarkdown lib/artistIntel/formatArtistIntelPackAsMarkdown.ts:101:53
❯ lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts:142:20


[failure] 101-101: lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts > formatArtistIntelPackAsMarkdown > includes audience profile
TypeError: Cannot read properties of undefined (reading 'overall')
❯ formatArtistIntelPackAsMarkdown lib/artistIntel/formatArtistIntelPackAsMarkdown.ts:101:53
❯ lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts:135:20


[failure] 101-101: lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts > formatArtistIntelPackAsMarkdown > includes music DNA section when analysis is available
TypeError: Cannot read properties of undefined (reading 'overall')
❯ formatArtistIntelPackAsMarkdown lib/artistIntel/formatArtistIntelPackAsMarkdown.ts:101:53
❯ lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts:127:20


[failure] 101-101: lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts > formatArtistIntelPackAsMarkdown > includes top track name and album
TypeError: Cannot read properties of undefined (reading 'overall')
❯ formatArtistIntelPackAsMarkdown lib/artistIntel/formatArtistIntelPackAsMarkdown.ts:101:53
❯ lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts:121:20


[failure] 101-101: lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts > formatArtistIntelPackAsMarkdown > includes genre information
TypeError: Cannot read properties of undefined (reading 'overall')
❯ formatArtistIntelPackAsMarkdown lib/artistIntel/formatArtistIntelPackAsMarkdown.ts:101:53
❯ lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts:115:20


[failure] 101-101: lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts > formatArtistIntelPackAsMarkdown > includes follower count and popularity
TypeError: Cannot read properties of undefined (reading 'overall')
❯ formatArtistIntelPackAsMarkdown lib/artistIntel/formatArtistIntelPackAsMarkdown.ts:101:53
❯ lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts:109:20


[failure] 101-101: lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts > formatArtistIntelPackAsMarkdown > includes elapsed time in the header
TypeError: Cannot read properties of undefined (reading 'overall')
❯ formatArtistIntelPackAsMarkdown lib/artistIntel/formatArtistIntelPackAsMarkdown.ts:101:53
❯ lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts:104:20


[failure] 101-101: lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts > formatArtistIntelPackAsMarkdown > includes the artist name in the heading
TypeError: Cannot read properties of undefined (reading 'overall')
❯ formatArtistIntelPackAsMarkdown lib/artistIntel/formatArtistIntelPackAsMarkdown.ts:101:53
❯ lib/artistIntel/tests/formatArtistIntelPackAsMarkdown.test.ts:99:20

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/formatArtistIntelPackAsMarkdown.ts` around lines 95 - 124,
The formatter currently assumes opportunity_scores exists and crashes; update
formatArtistIntelPackAsMarkdown to guard or normalize before pushing lines:
check if opportunity_scores is present on the pack (or create a default
opportunity_scores object with safe defaults for overall, sync/playlist/ar/brand
{score, rating, rationale}) and then use those safe values when calling
ratingEmoji(...) and scoreBar(...); alternatively wrap the whole Opportunity
Scores block in a presence check for opportunity_scores and skip it if missing,
ensuring all references (opportunity_scores.overall,
opportunity_scores.sync.score, opportunity_scores.sync.rating,
opportunity_scores.sync.rationale, and similarly for playlist/ar/brand) and
calls to ratingEmoji and scoreBar only run against validated/defaulted data.

Comment on lines +74 to +79
// All three data-fetch steps run in parallel
const [musicAnalysis, webContext, peerBenchmark] = await Promise.all([
spotifyData.previewUrl ? getArtistMusicAnalysis(spotifyData.previewUrl) : Promise.resolve(null),
getArtistWebContext(artistName),
getRelatedArtistsData(artist.id, artist.followers.total, artist.popularity),
]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make the enrichment fan-out fail soft.

These branches are optional enrichments, but Promise.all turns any rejection from MusicFlamingo, Perplexity, or related-artists lookup into a full pack failure. Please switch this block to safe fallbacks or Promise.allSettled so a provider outage degrades to null instead of dropping the entire response. As per coding guidelines, lib/**/*.ts: “Proper error handling”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/generateArtistIntelPack.ts` around lines 74 - 79, The current
Promise.all call for optional enrichments (getArtistMusicAnalysis,
getArtistWebContext, getRelatedArtistsData) must be made "fail soft": replace
the Promise.all with Promise.allSettled (or wrap each call in a try/catch) and
map any rejected result to null so that failures from MusicFlamingo, Perplexity,
or related-artists do not throw the whole pack; ensure spotifyData.previewUrl
still gates getArtistMusicAnalysis and that the resolved values are assigned
back to the same variables (musicAnalysis, webContext, peerBenchmark) with null
fallbacks on error.

Comment on lines +1 to +2
import generateAccessToken from "@/lib/spotify/generateAccessToken";
import { getRelatedArtists } from "@/lib/spotify/getRelatedArtists";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid credential-gating the whole module at import time.

generateAccessToken.ts throws when Spotify credentials are absent, so this top-level import can make any consumer of getRelatedArtistsData() fail during module evaluation instead of returning null. That matches the current test failure; please lazy-load the token helper here or move env validation into the helper body.

🧰 Tools
🪛 GitHub Actions: Unit Tests

[error] 1-1: Test suite failure caused by missing Spotify credentials (upstream error from generateAccessToken).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/getRelatedArtistsData.ts` around lines 1 - 2, The module
currently imports generateAccessToken at top-level which throws when Spotify
creds are missing; change to lazy-load it inside getRelatedArtistsData (or move
the env validation into generateAccessToken) so module evaluation never throws:
update getRelatedArtistsData to require/import or call generateAccessToken
dynamically at runtime before using it (and keep getRelatedArtists imported as
needed), ensuring when creds are absent the function returns null instead of
causing an import-time exception.

Comment on lines +44 to +48
// Take top 5 most-followed peers for a focused comparison
const peers: PeerArtist[] = related
.slice(0, 10)
.sort((a, b) => b.followers.total - a.followers.total)
.slice(0, 5)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Sort the full related-artist set before truncating it.

slice(0, 10).sort(...).slice(0, 5) only ranks the first 10 API results, not the full related-artist list. If Spotify returns a larger relevance-ordered set, the “top 5 most-followed peers” and all downstream medians/percentiles become arbitrary.

Possible fix
-  const peers: PeerArtist[] = related
-    .slice(0, 10)
-    .sort((a, b) => b.followers.total - a.followers.total)
-    .slice(0, 5)
+  const peers: PeerArtist[] = [...related]
+    .sort((a, b) => b.followers.total - a.followers.total)
+    .slice(0, 5)
     .map(a => ({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/getRelatedArtistsData.ts` around lines 44 - 48, The current
code slices related before sorting which only ranks the first 10 items; instead
sort the entire related array by followers (b.followers.total -
a.followers.total) and then take the top 5, i.e. replace the chain
"related.slice(0, 10).sort(...).slice(0, 5)" with a sort-first-then-slice
approach so the peers variable truly contains the top 5 most-followed artists;
update the peers assignment in getRelatedArtistsData (the peers/related logic)
accordingly.

Comment on lines +60 to +68
const median = (arr: number[]) => {
const mid = Math.floor(arr.length / 2);
return arr.length % 2 !== 0 ? arr[mid] : Math.round((arr[mid - 1] + arr[mid]) / 2);
};

const percentile = (arr: number[], value: number) => {
const below = arr.filter(v => v < value).length;
return Math.round((below / arr.length) * 100);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Percentile rank should account for ties.

With the current v < value formula, an artist tied with every peer shows up as the 0th percentile. Popularity is a 0–100 integer, so ties are common, and this also skews the A&R bonus that consumes popularity_percentile.

Possible fix
   const percentile = (arr: number[], value: number) => {
     const below = arr.filter(v => v < value).length;
-    return Math.round((below / arr.length) * 100);
+    const equal = arr.filter(v => v === value).length;
+    return Math.round(((below + equal / 2) / arr.length) * 100);
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/getRelatedArtistsData.ts` around lines 60 - 68, Percentile
calculation in the percentile(arr:number[], value:number) function ignores ties
(uses v < value), which makes tied popularities report 0; change the logic to
count values strictly less (countLess) and values equal (countEqual) and compute
percentile as round(((countLess + 0.5 * countEqual) / arr.length) * 100), and
handle empty arrays defensively (e.g., return 0); update the percentile function
(and any callers that expect popularity_percentile) to use this tied-aware
formula while leaving median(...) unchanged.

Comment on lines +15 to +19
const response = await fetch(`https://api.spotify.com/v1/artists/${artistId}/related-artists`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Add a timeout to the Spotify request.

This call has no cancellation path, so a slow Spotify edge can stall the whole pack generation until the platform-level timeout kicks in. Please add an explicit timeout/abort and treat that the same way you already treat other soft failures here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/spotify/getRelatedArtists.ts` around lines 15 - 19, The Spotify fetch in
getRelatedArtists currently has no cancellation; wrap the request with an
AbortController and a short timeout (e.g., 2–5s) that calls controller.abort()
via setTimeout, pass controller.signal to the fetch call, and catch the abort
error; in the getRelatedArtists function treat an aborted/timeout error the same
as other soft failures (returning the existing fallback path), and ensure you
clear the timeout on success to avoid leaks.

…coverage

- computeArtistOpportunityScores: fix misleading "established/saturated" A&R
  rationale for artists with <1000 followers AND <10 popularity. These are
  pre-market acts (e.g. Gatsby Grace: 2 followers, 0 popularity), not
  saturated ones. Now correctly says "Pre-market artist — highest early-
  discovery upside" and awards a +10 discovery bonus.
- Add computeArtistOpportunityScores.test.ts with 6 tests covering pre-market
  detection, high-follower low-popularity case, music analysis paths, and
  overall score weighting.
- generateArtistIntelPack.test.ts: add Gatsby Grace scenario (8 tests) using
  real Spotify data (Spotify ID: 7ljukJB2Ctl0T4vCoYfb2x, 2 followers, 0
  popularity, no genres). Mock now also covers getRelatedArtistsData.
- formatArtistIntelPackAsMarkdown.test.ts: add missing peer_benchmark,
  opportunity_scores, and catalog_depth fields to basePack — these were added
  in a prior upgrade but the test mock was never updated (21 tests now pass).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
lib/artistIntel/computeArtistOpportunityScores.ts (1)

46-280: Split the scorer into smaller pure helpers.

computeArtistOpportunityScores now owns four separate scoring algorithms, rationale assembly, clamping, and weighting in one ~230-line function. Extracting per-domain helpers plus shared score-building utilities will make threshold changes much safer to review and test.

♻️ Refactor sketch
+function clampScore(score: number): number {
+  return Math.min(100, Math.max(0, score));
+}
+
+function toOpportunityScore(
+  score: number,
+  rationale: string[],
+  fallback: string,
+): OpportunityScore {
+  const clamped = clampScore(score);
+  return {
+    score: clamped,
+    rating: rating(clamped),
+    rationale: rationale.join(". ") || fallback,
+  };
+}
+
+function computeSyncOpportunityScore(
+  musicAnalysis: ArtistMusicAnalysis | null,
+  popularity: number,
+): OpportunityScore {
+  // sync-specific logic only
+}
+
+function computePlaylistOpportunityScore(
+  musicAnalysis: ArtistMusicAnalysis | null,
+  popularity: number,
+): OpportunityScore {
+  // playlist-specific logic only
+}
+
+function computeArOpportunityScore(
+  followers: number,
+  popularity: number,
+  peerBenchmark: PeerBenchmark | null,
+): OpportunityScore {
+  // A&R-specific logic only
+}
+
+function computeBrandOpportunityScore(
+  musicAnalysis: ArtistMusicAnalysis | null,
+  popularity: number,
+): OpportunityScore {
+  // brand-specific logic only
+}
+
 export function computeArtistOpportunityScores(...) {
-  // all scoring logic inline
-  return {
-    sync: { ... },
-    playlist: { ... },
-    ar: { ... },
-    brand: { ... },
-    overall,
-  };
+  const sync = computeSyncOpportunityScore(musicAnalysis, popularity);
+  const playlist = computePlaylistOpportunityScore(musicAnalysis, popularity);
+  const ar = computeArOpportunityScore(followers, popularity, peerBenchmark);
+  const brand = computeBrandOpportunityScore(musicAnalysis, popularity);
+  const overall = Math.round(
+    ar.score * 0.3 + playlist.score * 0.25 + sync.score * 0.25 + brand.score * 0.2,
+  );
+  return { sync, playlist, ar, brand, overall };
 }

As per coding guidelines, lib/**/*.ts: "Single responsibility per function", "Keep functions under 50 lines", and "DRY: Consolidate similar logic into shared utilities".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/computeArtistOpportunityScores.ts` around lines 46 - 280, The
computeArtistOpportunityScores function is doing four distinct scoring workflows
plus common tasks (clamping, rating, rationale assembly, weighting); extract
pure helper functions computeSyncScore, computePlaylistScore, computeARScore,
and computeBrandScore that each accept the minimal inputs
(musicAnalysis/catalog_metadata, followers, popularity, peerBenchmark as needed)
and return { score:number, rationale:string[] }, and pull shared utilities like
clampScore(score), buildRationale(rationaleArray) and any numeric helpers (e.g.,
followerEfficiency) into small pure functions; then reduce
computeArtistOpportunityScores to calling these helpers, clamping/ratifying
results with clampScore and rating(), joining rationales with buildRationale,
and computing the overall weighted score—ensure all original symbols
(syncRationale/playlistRationale/arRationale/brandRationale, rating(), and
overall weighting) are preserved so behavior remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/artistIntel/computeArtistOpportunityScores.ts`:
- Around line 151-168: The pre-market check is being bypassed because
followerEfficiency is evaluated first; move the pre-market condition (check
followers < 1000 && popularity < 10) before the followerEfficiency tiers so
tiny-but-nonzero artists (e.g., followers=10, popularity=9) get the pre-market
bonus; update computeArtistOpportunityScores to evaluate the pre-market branch
prior to computing/applying the followerEfficiency > 12 / > 8 branches and still
push the same arScore and arRationale entries when that pre-market condition
matches.
- Around line 176-189: The peer-gap and median-gap strings currently use whole-K
rounding (e.g., ((...)/1000).toFixed(0) and (followerGap/1000).toFixed(0)) which
yields misleading "0K" or coarse values; update the formatting logic where
followerGap, peerBenchmark.median_followers and the corresponding arRationale
pushes are built (look for variables followerGap,
peerBenchmark.median_followers, arRationale and peerBenchmark.top_peer?.name) to
format deltas < 1000 as raw integer counts (e.g., "499 below peer median") and
deltas >= 1000 with one decimal K precision (e.g., "1.2K gap to top peer");
implement this either via a small helper formatDelta(delta) used in both the
median and top-peer messages or inline conditional formatting before pushing to
arRationale.

---

Nitpick comments:
In `@lib/artistIntel/computeArtistOpportunityScores.ts`:
- Around line 46-280: The computeArtistOpportunityScores function is doing four
distinct scoring workflows plus common tasks (clamping, rating, rationale
assembly, weighting); extract pure helper functions computeSyncScore,
computePlaylistScore, computeARScore, and computeBrandScore that each accept the
minimal inputs (musicAnalysis/catalog_metadata, followers, popularity,
peerBenchmark as needed) and return { score:number, rationale:string[] }, and
pull shared utilities like clampScore(score), buildRationale(rationaleArray) and
any numeric helpers (e.g., followerEfficiency) into small pure functions; then
reduce computeArtistOpportunityScores to calling these helpers,
clamping/ratifying results with clampScore and rating(), joining rationales with
buildRationale, and computing the overall weighted score—ensure all original
symbols (syncRationale/playlistRationale/arRationale/brandRationale, rating(),
and overall weighting) are preserved so behavior remains unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8aab9895-99dd-408b-b1b7-603480e0473b

📥 Commits

Reviewing files that changed from the base of the PR and between 52ee805 and 919ea62.

⛔ Files ignored due to path filters (3)
  • lib/artistIntel/__tests__/computeArtistOpportunityScores.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/artistIntel/__tests__/formatArtistIntelPackAsMarkdown.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/artistIntel/__tests__/generateArtistIntelPack.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (1)
  • lib/artistIntel/computeArtistOpportunityScores.ts

Comment on lines +151 to +168
const followerEfficiency = followers > 0 ? popularity / Math.log10(followers + 1) : 0;

if (followerEfficiency > 12) {
arScore += 25;
arRationale.push(
`High popularity-to-follower ratio (${followerEfficiency.toFixed(1)}) — strong algorithmic pull without mass audience`,
);
} else if (followerEfficiency > 8) {
arScore += 15;
arRationale.push(
`Solid popularity-to-follower ratio (${followerEfficiency.toFixed(1)}) indicates organic traction`,
);
} else if (followers < 1000 && popularity < 10) {
// Pre-market artist: very low followers AND very low popularity = early discovery opportunity
arScore += 10;
arRationale.push(
`Pre-market artist (${followers} followers, ${popularity}/100 popularity) — no Spotify traction yet, highest early-discovery upside`,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Check the pre-market rule before the efficiency tiers.

A tiny-but-nonzero artist can miss the new pre-market classification here. For example, followers = 10 and popularity = 9 gives followerEfficiency ≈ 8.6, so this code takes the "organic traction" branch before it ever reaches the intended <1000 && <10 pre-market bonus/rationale.

🐛 Suggested fix
-  if (followerEfficiency > 12) {
+  if (followers < 1000 && popularity < 10) {
+    // Pre-market artist: very low followers AND very low popularity = early discovery opportunity
+    arScore += 10;
+    arRationale.push(
+      `Pre-market artist (${followers} followers, ${popularity}/100 popularity) — no Spotify traction yet, highest early-discovery upside`,
+    );
+  } else if (followerEfficiency > 12) {
     arScore += 25;
     arRationale.push(
       `High popularity-to-follower ratio (${followerEfficiency.toFixed(1)}) — strong algorithmic pull without mass audience`,
     );
   } else if (followerEfficiency > 8) {
     arScore += 15;
     arRationale.push(
       `Solid popularity-to-follower ratio (${followerEfficiency.toFixed(1)}) indicates organic traction`,
     );
-  } else if (followers < 1000 && popularity < 10) {
-    // Pre-market artist: very low followers AND very low popularity = early discovery opportunity
-    arScore += 10;
-    arRationale.push(
-      `Pre-market artist (${followers} followers, ${popularity}/100 popularity) — no Spotify traction yet, highest early-discovery upside`,
-    );
   } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const followerEfficiency = followers > 0 ? popularity / Math.log10(followers + 1) : 0;
if (followerEfficiency > 12) {
arScore += 25;
arRationale.push(
`High popularity-to-follower ratio (${followerEfficiency.toFixed(1)}) — strong algorithmic pull without mass audience`,
);
} else if (followerEfficiency > 8) {
arScore += 15;
arRationale.push(
`Solid popularity-to-follower ratio (${followerEfficiency.toFixed(1)}) indicates organic traction`,
);
} else if (followers < 1000 && popularity < 10) {
// Pre-market artist: very low followers AND very low popularity = early discovery opportunity
arScore += 10;
arRationale.push(
`Pre-market artist (${followers} followers, ${popularity}/100 popularity) — no Spotify traction yet, highest early-discovery upside`,
);
const followerEfficiency = followers > 0 ? popularity / Math.log10(followers + 1) : 0;
if (followers < 1000 && popularity < 10) {
// Pre-market artist: very low followers AND very low popularity = early discovery opportunity
arScore += 10;
arRationale.push(
`Pre-market artist (${followers} followers, ${popularity}/100 popularity) — no Spotify traction yet, highest early-discovery upside`,
);
} else if (followerEfficiency > 12) {
arScore += 25;
arRationale.push(
`High popularity-to-follower ratio (${followerEfficiency.toFixed(1)}) — strong algorithmic pull without mass audience`,
);
} else if (followerEfficiency > 8) {
arScore += 15;
arRationale.push(
`Solid popularity-to-follower ratio (${followerEfficiency.toFixed(1)}) indicates organic traction`,
);
} else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/computeArtistOpportunityScores.ts` around lines 151 - 168,
The pre-market check is being bypassed because followerEfficiency is evaluated
first; move the pre-market condition (check followers < 1000 && popularity < 10)
before the followerEfficiency tiers so tiny-but-nonzero artists (e.g.,
followers=10, popularity=9) get the pre-market bonus; update
computeArtistOpportunityScores to evaluate the pre-market branch prior to
computing/applying the followerEfficiency > 12 / > 8 branches and still push the
same arScore and arRationale entries when that pre-market condition matches.

Comment on lines +176 to +189
const followerGap = peerBenchmark.top_peer ? peerBenchmark.top_peer.followers - followers : 0;

if (followers < peerBenchmark.median_followers) {
arScore += 20;
arRationale.push(
`${((peerBenchmark.median_followers - followers) / 1000).toFixed(0)}K below peer median — significant upside runway`,
);
} else {
arRationale.push("At or above peer median — focus on next tier of growth");
}

if (followerGap > 0) {
arRationale.push(
`${(followerGap / 1000).toFixed(0)}K gap to top peer (${peerBenchmark.top_peer?.name}) shows potential ceiling`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid whole-K rounding in the peer-gap copy.

These strings can emit 0K below peer median for a 499-follower gap and 1K for 500, which makes the new peer benchmarking output look less trustworthy than the underlying data. Format sub-1K deltas as raw counts and use at least one decimal for larger gaps.

🧮 Suggested fix
+    const formatFollowerGap = (value: number) =>
+      value < 1000 ? value.toLocaleString() : `${(value / 1000).toFixed(1)}K`;
+
     if (followers < peerBenchmark.median_followers) {
       arScore += 20;
       arRationale.push(
-        `${((peerBenchmark.median_followers - followers) / 1000).toFixed(0)}K below peer median — significant upside runway`,
+        `${formatFollowerGap(peerBenchmark.median_followers - followers)} below peer median — significant upside runway`,
       );
     } else {
       arRationale.push("At or above peer median — focus on next tier of growth");
     }

     if (followerGap > 0) {
       arRationale.push(
-        `${(followerGap / 1000).toFixed(0)}K gap to top peer (${peerBenchmark.top_peer?.name}) shows potential ceiling`,
+        `${formatFollowerGap(followerGap)} gap to top peer (${peerBenchmark.top_peer?.name}) shows potential ceiling`,
       );
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const followerGap = peerBenchmark.top_peer ? peerBenchmark.top_peer.followers - followers : 0;
if (followers < peerBenchmark.median_followers) {
arScore += 20;
arRationale.push(
`${((peerBenchmark.median_followers - followers) / 1000).toFixed(0)}K below peer median — significant upside runway`,
);
} else {
arRationale.push("At or above peer median — focus on next tier of growth");
}
if (followerGap > 0) {
arRationale.push(
`${(followerGap / 1000).toFixed(0)}K gap to top peer (${peerBenchmark.top_peer?.name}) shows potential ceiling`,
const followerGap = peerBenchmark.top_peer ? peerBenchmark.top_peer.followers - followers : 0;
const formatFollowerGap = (value: number) =>
value < 1000 ? value.toLocaleString() : `${(value / 1000).toFixed(1)}K`;
if (followers < peerBenchmark.median_followers) {
arScore += 20;
arRationale.push(
`${formatFollowerGap(peerBenchmark.median_followers - followers)} below peer median — significant upside runway`,
);
} else {
arRationale.push("At or above peer median — focus on next tier of growth");
}
if (followerGap > 0) {
arRationale.push(
`${formatFollowerGap(followerGap)} gap to top peer (${peerBenchmark.top_peer?.name}) shows potential ceiling`,
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/artistIntel/computeArtistOpportunityScores.ts` around lines 176 - 189,
The peer-gap and median-gap strings currently use whole-K rounding (e.g.,
((...)/1000).toFixed(0) and (followerGap/1000).toFixed(0)) which yields
misleading "0K" or coarse values; update the formatting logic where followerGap,
peerBenchmark.median_followers and the corresponding arRationale pushes are
built (look for variables followerGap, peerBenchmark.median_followers,
arRationale and peerBenchmark.top_peer?.name) to format deltas < 1000 as raw
integer counts (e.g., "499 below peer median") and deltas >= 1000 with one
decimal K precision (e.g., "1.2K gap to top peer"); implement this either via a
small helper formatDelta(delta) used in both the median and top-peer messages or
inline conditional formatting before pushing to arRationale.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant